mirror of https://github.com/portainer/portainer
feat(home): change layout of env tile [EE-4479] (#8061)
parent
b48aa1274d
commit
eba5879ec8
|
@ -205,7 +205,6 @@ input[type='checkbox'] {
|
||||||
.blocklist-item {
|
.blocklist-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--border-blocklist);
|
border: 1px solid var(--border-blocklist);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -222,8 +221,9 @@ input[type='checkbox'] {
|
||||||
color: var(--text-blocklist-item-selected-color);
|
color: var(--text-blocklist-item-selected-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklist-item:hover {
|
.blocklist-item:not(.blocklist-item-not-interactive):hover {
|
||||||
@apply border border-blue-7;
|
@apply border border-blue-7;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
background-color: var(--bg-blocklist-hover-color);
|
background-color: var(--bg-blocklist-hover-color);
|
||||||
color: var(--text-blocklist-hover-color);
|
color: var(--text-blocklist-hover-color);
|
||||||
|
@ -233,10 +233,6 @@ input[type='checkbox'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklist-item-line.endpoint-item {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklist-item-line {
|
.blocklist-item-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -248,24 +244,11 @@ input[type='checkbox'] {
|
||||||
max-height: 60px;
|
max-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklist-item-logo.endpoint-item {
|
|
||||||
margin: 10px 4px 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklist-item-logo.endpoint-item.azure {
|
|
||||||
margin: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklist-item-title {
|
.blocklist-item-title {
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklist-item-title.endpoint-item {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklist-item-subtitle {
|
.blocklist-item-subtitle {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: space-around;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
export type UserId = number;
|
import { UserId } from './types/user-id';
|
||||||
|
|
||||||
|
export { type UserId };
|
||||||
|
|
||||||
export enum Role {
|
export enum Role {
|
||||||
Admin = 1,
|
Admin = 1,
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export type UserId = number;
|
|
@ -78,6 +78,7 @@ export function createMockEnvironment(): Environment {
|
||||||
AllowNoneIngressClass: false,
|
AllowNoneIngressClass: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Nomad: { Snapshots: [] },
|
||||||
EdgeKey: '',
|
EdgeKey: '',
|
||||||
Id: 3,
|
Id: 3,
|
||||||
UserTrusted: false,
|
UserTrusted: false,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import clsx from 'clsx';
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||||
|
import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat';
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
|
||||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
import { EnvironmentStatusBadgeItem } from './EnvironmentStatusBadgeItem';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showLastCheckInDate?: boolean;
|
showLastCheckInDate?: boolean;
|
||||||
|
@ -15,100 +16,46 @@ export function EdgeIndicator({
|
||||||
|
|
||||||
showLastCheckInDate = false,
|
showLastCheckInDate = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const associated = !!environment.EdgeID;
|
const isValid = useHasHeartbeat(environment);
|
||||||
|
|
||||||
const isValid = useHasHeartbeat(environment, associated);
|
|
||||||
|
|
||||||
if (isValid === null) {
|
if (isValid === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const associated = !!environment.EdgeID;
|
||||||
if (!associated) {
|
if (!associated) {
|
||||||
return (
|
return (
|
||||||
<span role="status" aria-label="edge-status">
|
<span role="status" aria-label="edge-status">
|
||||||
<span className="label label-default" aria-label="unassociated">
|
<EnvironmentStatusBadgeItem aria-label="unassociated">
|
||||||
<s>associated</s>
|
<s>associated</s>
|
||||||
</span>
|
</EnvironmentStatusBadgeItem>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span role="status" aria-label="edge-status">
|
|
||||||
<span
|
<span
|
||||||
className={clsx('label', {
|
role="status"
|
||||||
'label-danger': !isValid,
|
aria-label="edge-status"
|
||||||
'label-success': isValid,
|
className="flex items-center gap-1"
|
||||||
})}
|
>
|
||||||
|
<EnvironmentStatusBadgeItem
|
||||||
|
color={isValid ? 'success' : 'danger'}
|
||||||
aria-label="edge-heartbeat"
|
aria-label="edge-heartbeat"
|
||||||
>
|
>
|
||||||
heartbeat
|
heartbeat
|
||||||
</span>
|
</EnvironmentStatusBadgeItem>
|
||||||
|
|
||||||
{showLastCheckInDate && !!environment.LastCheckInDate && (
|
{showLastCheckInDate && !!environment.LastCheckInDate && (
|
||||||
<span
|
<span
|
||||||
className="space-left small text-muted"
|
className="small text-muted vertical-center"
|
||||||
aria-label="edge-last-checkin"
|
aria-label="edge-last-checkin"
|
||||||
|
title="Last edge check-in"
|
||||||
>
|
>
|
||||||
|
<Activity className="icon icon-sm space-right" aria-hidden="true" />
|
||||||
{isoDateFromTimestamp(environment.LastCheckInDate)}
|
{isoDateFromTimestamp(environment.LastCheckInDate)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHasHeartbeat(environment: Environment, associated: boolean) {
|
|
||||||
const settingsQuery = usePublicSettings({ enabled: associated });
|
|
||||||
|
|
||||||
if (!associated) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { LastCheckInDate, QueryDate } = environment;
|
|
||||||
|
|
||||||
const settings = settingsQuery.data;
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkInInterval = getCheckinInterval(environment, settings);
|
|
||||||
|
|
||||||
if (checkInInterval && QueryDate && LastCheckInDate) {
|
|
||||||
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCheckinInterval(
|
|
||||||
environment: Environment,
|
|
||||||
settings: PublicSettingsViewModel
|
|
||||||
) {
|
|
||||||
const asyncMode = environment.Edge.AsyncMode;
|
|
||||||
|
|
||||||
if (asyncMode) {
|
|
||||||
const intervals = [
|
|
||||||
environment.Edge.PingInterval > 0
|
|
||||||
? environment.Edge.PingInterval
|
|
||||||
: settings.Edge.PingInterval,
|
|
||||||
environment.Edge.SnapshotInterval > 0
|
|
||||||
? environment.Edge.SnapshotInterval
|
|
||||||
: settings.Edge.SnapshotInterval,
|
|
||||||
environment.Edge.CommandInterval > 0
|
|
||||||
? environment.Edge.CommandInterval
|
|
||||||
: settings.Edge.CommandInterval,
|
|
||||||
].filter((n) => n > 0);
|
|
||||||
|
|
||||||
return intervals.length > 0 ? Math.min(...intervals) : 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!environment.EdgeCheckinInterval ||
|
|
||||||
environment.EdgeCheckinInterval === 0
|
|
||||||
) {
|
|
||||||
return settings.Edge.CheckinInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
return environment.EdgeCheckinInterval;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EnvironmentStatusBadgeItem } from './EnvironmentStatusBadgeItem';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
status: EnvironmentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnvironmentStatusBadge({ status }: Props) {
|
||||||
|
return status === EnvironmentStatus.Up ? (
|
||||||
|
<EnvironmentStatusBadgeItem color="success" icon={CheckCircle}>
|
||||||
|
Up
|
||||||
|
</EnvironmentStatusBadgeItem>
|
||||||
|
) : (
|
||||||
|
<EnvironmentStatusBadgeItem color="danger" icon={XCircle}>
|
||||||
|
Down
|
||||||
|
</EnvironmentStatusBadgeItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { AriaAttributes, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { Icon, IconProps } from '@@/Icon';
|
||||||
|
|
||||||
|
export function EnvironmentStatusBadgeItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
color = 'default',
|
||||||
|
icon,
|
||||||
|
...aria
|
||||||
|
}: PropsWithChildren<
|
||||||
|
{
|
||||||
|
className?: string;
|
||||||
|
color?: 'success' | 'danger' | 'default';
|
||||||
|
icon?: IconProps['icon'];
|
||||||
|
} & AriaAttributes
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'flex items-center gap-1',
|
||||||
|
'border-2 border-solid rounded',
|
||||||
|
'w-fit py-px px-1',
|
||||||
|
'text-xs font-semibold text-gray-7',
|
||||||
|
{
|
||||||
|
'border-green-3 bg-green-2': color === 'success',
|
||||||
|
'border-error-3 bg-error-2': color === 'danger',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...aria}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<Icon
|
||||||
|
icon={icon}
|
||||||
|
className={clsx({
|
||||||
|
'!text-green-7': color === 'success',
|
||||||
|
'!text-error-7': color === 'danger',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
import { Button } from './buttons';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
|
export function LinkButton({
|
||||||
|
to,
|
||||||
|
params,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof Button> & ComponentProps<typeof Link>) {
|
||||||
|
const button = (
|
||||||
|
<Button
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
size="medium"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={to} params={params} className="text-inherit hover:no-underline">
|
||||||
|
{button}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,20 +9,19 @@ interface Props extends IconProps {
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EnvironmentStatsItem({
|
export function StatsItem({
|
||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
iconClass,
|
iconClass,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<span className="vertical-center space-right">
|
<span className="flex gap-1 items-center">
|
||||||
<Icon
|
<Icon className={clsx('icon icon-sm', iconClass)} icon={icon} />
|
||||||
className={clsx('icon icon-sm space-right', iconClass)}
|
|
||||||
icon={icon}
|
|
||||||
/>
|
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
{children && <span className="space-left">{children}</span>}
|
{children && (
|
||||||
|
<span className="ml-1 flex gap-2 items-center">{children}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
|
||||||
|
export type DockerSnapshotRaw = {
|
||||||
|
Containers: DockerContainer[];
|
||||||
|
SnapshotTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DockerSnapshot {
|
||||||
|
TotalCPU: number;
|
||||||
|
TotalMemory: number;
|
||||||
|
NodeCount: number;
|
||||||
|
ImageCount: number;
|
||||||
|
VolumeCount: number;
|
||||||
|
RunningContainerCount: number;
|
||||||
|
StoppedContainerCount: number;
|
||||||
|
HealthyContainerCount: number;
|
||||||
|
UnhealthyContainerCount: number;
|
||||||
|
Time: number;
|
||||||
|
StackCount: number;
|
||||||
|
ServiceCount: number;
|
||||||
|
Swarm: boolean;
|
||||||
|
DockerVersion: string;
|
||||||
|
GpuUseAll: boolean;
|
||||||
|
GpuUseList: string[];
|
||||||
|
SnapshotRaw: DockerSnapshotRaw;
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat';
|
||||||
import { EdgeIndicator } from '@@/EdgeIndicator';
|
|
||||||
|
|
||||||
export const heartbeat: Column<Environment> = {
|
export const heartbeat: Column<Environment> = {
|
||||||
Header: 'Heartbeat',
|
Header: 'Heartbeat',
|
||||||
|
@ -18,3 +18,36 @@ export function StatusCell({
|
||||||
}: CellProps<Environment>) {
|
}: CellProps<Environment>) {
|
||||||
return <EdgeIndicator environment={environment} />;
|
return <EdgeIndicator environment={environment} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EdgeIndicator({ environment }: { environment: Environment }) {
|
||||||
|
const isValid = useHasHeartbeat(environment);
|
||||||
|
|
||||||
|
if (isValid === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const associated = !!environment.EdgeID;
|
||||||
|
if (!associated) {
|
||||||
|
return (
|
||||||
|
<span role="status" aria-label="edge-status">
|
||||||
|
<span className="label label-default" aria-label="unassociated">
|
||||||
|
<s>associated</s>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span role="status" aria-label="edge-status">
|
||||||
|
<span
|
||||||
|
className={clsx('label', {
|
||||||
|
'label-danger': !isValid,
|
||||||
|
'label-success': isValid,
|
||||||
|
})}
|
||||||
|
aria-label="edge-heartbeat"
|
||||||
|
>
|
||||||
|
heartbeat
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||||
|
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||||
|
|
||||||
|
export function useHasHeartbeat(environment: Environment) {
|
||||||
|
const associated = !!environment.EdgeID;
|
||||||
|
|
||||||
|
const settingsQuery = usePublicSettings({ enabled: associated });
|
||||||
|
|
||||||
|
if (!associated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { LastCheckInDate, QueryDate } = environment;
|
||||||
|
|
||||||
|
const settings = settingsQuery.data;
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInInterval = getCheckinInterval(environment, settings);
|
||||||
|
|
||||||
|
if (checkInInterval && QueryDate && LastCheckInDate) {
|
||||||
|
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCheckinInterval(
|
||||||
|
environment: Environment,
|
||||||
|
settings: PublicSettingsViewModel
|
||||||
|
) {
|
||||||
|
const asyncMode = environment.Edge.AsyncMode;
|
||||||
|
|
||||||
|
if (asyncMode) {
|
||||||
|
const intervals = [
|
||||||
|
environment.Edge.PingInterval > 0
|
||||||
|
? environment.Edge.PingInterval
|
||||||
|
: settings.Edge.PingInterval,
|
||||||
|
environment.Edge.SnapshotInterval > 0
|
||||||
|
? environment.Edge.SnapshotInterval
|
||||||
|
: settings.Edge.SnapshotInterval,
|
||||||
|
environment.Edge.CommandInterval > 0
|
||||||
|
? environment.Edge.CommandInterval
|
||||||
|
: settings.Edge.CommandInterval,
|
||||||
|
].filter((n) => n > 0);
|
||||||
|
|
||||||
|
return intervals.length > 0 ? Math.min(...intervals) : 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!environment.EdgeCheckinInterval ||
|
||||||
|
environment.EdgeCheckinInterval === 0
|
||||||
|
) {
|
||||||
|
return settings.Edge.CheckinInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment.EdgeCheckinInterval;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Power } from 'lucide-react';
|
import { Power } from 'lucide-react';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { StatsItem } from '@@/StatsItem';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
running: number;
|
running: number;
|
||||||
|
@ -11,11 +11,19 @@ export function RunningStatus({ running, stopped }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Icon icon={Power} mode="success" />
|
<StatsItem
|
||||||
|
value={`${running || '-'} running`}
|
||||||
|
icon={Power}
|
||||||
|
iconClass="icon-success"
|
||||||
|
/>
|
||||||
{`${running || '-'} running`}
|
{`${running || '-'} running`}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Icon icon={Power} mode="danger" />
|
<StatsItem
|
||||||
|
value={`${stopped || '-'} stopped`}
|
||||||
|
icon={Power}
|
||||||
|
iconClass="icon-danger"
|
||||||
|
/>
|
||||||
{`${stopped || '-'} stopped`}
|
{`${stopped || '-'} stopped`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,9 +18,8 @@ export function AgentVersionTag({ type, version }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="space-x-1">
|
<span className="space-x-1">
|
||||||
<span>
|
|
||||||
<Zap className="icon icon-xs vertical-center" aria-hidden="true" />
|
<Zap className="icon icon-xs vertical-center" aria-hidden="true" />
|
||||||
</span>
|
|
||||||
<span>{isEdgeEnvironment(type) ? 'Edge Agent' : 'Agent'}</span>
|
<span>{isEdgeEnvironment(type) ? 'Edge Agent' : 'Agent'}</span>
|
||||||
|
|
||||||
<span>{version}</span>
|
<span>{version}</span>
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Edit2, Settings } from 'lucide-react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useUser } from '@/react/hooks/useUser';
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
PlatformType,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
import {
|
||||||
|
isEdgeAsync as checkEdgeAsync,
|
||||||
|
getPlatformType,
|
||||||
|
} from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
|
import { LinkButton } from '@@/LinkButton';
|
||||||
|
|
||||||
|
export function EditButtons({ environment }: { environment: Environment }) {
|
||||||
|
const { isAdmin } = useUser();
|
||||||
|
|
||||||
|
const isEdgeAsync = checkEdgeAsync(environment);
|
||||||
|
|
||||||
|
const configRoute = getConfigRoute(environment);
|
||||||
|
return (
|
||||||
|
<ButtonsGrid className="w-11 -m-[11px] ml-3">
|
||||||
|
<LinkButton
|
||||||
|
disabled={!isAdmin}
|
||||||
|
to="portainer.endpoints.endpoint"
|
||||||
|
params={{ id: environment.Id }}
|
||||||
|
color="none"
|
||||||
|
icon={Edit2}
|
||||||
|
size="medium"
|
||||||
|
className="w-full h-full !ml-0 hover:bg-gray-3 !rounded-none"
|
||||||
|
title="Edit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkButton
|
||||||
|
disabled={!configRoute || isEdgeAsync || !isAdmin}
|
||||||
|
to={configRoute}
|
||||||
|
params={{ endpointId: environment.Id }}
|
||||||
|
color="none"
|
||||||
|
icon={Settings}
|
||||||
|
size="medium"
|
||||||
|
className="w-full h-full !ml-0 hover:bg-gray-3 !rounded-none"
|
||||||
|
title="Configuration"
|
||||||
|
/>
|
||||||
|
</ButtonsGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigRoute(environment: Environment) {
|
||||||
|
const platform = getPlatformType(environment.Type);
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case PlatformType.Docker:
|
||||||
|
return getDockerConfigRoute(environment);
|
||||||
|
case PlatformType.Kubernetes:
|
||||||
|
return 'kubernetes.cluster';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDockerConfigRoute(environment: Environment) {
|
||||||
|
const snapshot = environment.Snapshots?.[0];
|
||||||
|
if (!snapshot) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Swarm ? 'docker.swarm' : 'docker.host';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonsGrid({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'grid grid-rows-3 border border-solid border-gray-5 rounded-r-lg',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>{children[0] || null}</div>
|
||||||
|
<div className="border-x-0 border-y border-gray-5 border-solid">
|
||||||
|
{children[1] || null}
|
||||||
|
</div>
|
||||||
|
<div>{children[2] || null}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { DockerSnapshot } from '@/react/docker/snapshots/types';
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
PlatformType,
|
||||||
|
KubernetesSnapshot,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
import { getPlatformType } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
|
export function EngineVersion({ environment }: { environment: Environment }) {
|
||||||
|
const platform = getPlatformType(environment.Type);
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case PlatformType.Docker:
|
||||||
|
return <DockerEngineVersion snapshot={environment.Snapshots[0]} />;
|
||||||
|
case PlatformType.Kubernetes:
|
||||||
|
return (
|
||||||
|
<KubernetesEngineVersion
|
||||||
|
snapshot={environment.Kubernetes.Snapshots?.[0]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DockerEngineVersion({ snapshot }: { snapshot?: DockerSnapshot }) {
|
||||||
|
if (!snapshot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="small text-muted vertical-center">
|
||||||
|
{snapshot.Swarm ? 'Swarm' : 'Standalone'} {snapshot.DockerVersion}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KubernetesEngineVersion({
|
||||||
|
snapshot,
|
||||||
|
}: {
|
||||||
|
snapshot?: KubernetesSnapshot;
|
||||||
|
}) {
|
||||||
|
if (!snapshot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="small text-muted vertical-center">
|
||||||
|
Kubernetes {snapshot.KubernetesVersion}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Wifi, WifiOff } from 'lucide-react';
|
||||||
|
|
||||||
|
import ClockRewind from '@/assets/ico/clock-rewind.svg?c';
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import {
|
||||||
|
getDashboardRoute,
|
||||||
|
isEdgeAsync as checkEdgeAsync,
|
||||||
|
} from '@/react/portainer/environments/utils';
|
||||||
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { LinkButton } from '@@/LinkButton';
|
||||||
|
|
||||||
|
export function EnvironmentBrowseButtons({
|
||||||
|
environment,
|
||||||
|
onClickBrowse,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
environment: Environment;
|
||||||
|
onClickBrowse(): void;
|
||||||
|
isActive: boolean;
|
||||||
|
}) {
|
||||||
|
const isEdgeAsync = checkEdgeAsync(environment);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1 ml-auto [&>*]:flex-1">
|
||||||
|
{isBE && (
|
||||||
|
<LinkButton
|
||||||
|
icon={ClockRewind}
|
||||||
|
disabled={!isEdgeAsync}
|
||||||
|
to="edge.browse.dashboard"
|
||||||
|
params={{
|
||||||
|
environmentId: environment.Id,
|
||||||
|
}}
|
||||||
|
color="light"
|
||||||
|
className="w-full py-1"
|
||||||
|
>
|
||||||
|
Browse snapshot
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LinkButton
|
||||||
|
icon={Wifi}
|
||||||
|
disabled={isEdgeAsync}
|
||||||
|
to={getDashboardRoute(environment)}
|
||||||
|
params={{
|
||||||
|
endpointId: environment.Id,
|
||||||
|
}}
|
||||||
|
onClick={onClickBrowse}
|
||||||
|
color="primary"
|
||||||
|
className="w-full py-1"
|
||||||
|
>
|
||||||
|
Live connect
|
||||||
|
</LinkButton>
|
||||||
|
|
||||||
|
{!isActive ? (
|
||||||
|
<div className="min-h-[30px] vertical-center justify-center">
|
||||||
|
<Icon icon={WifiOff} />
|
||||||
|
Disconnected
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="min-h-[30px]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
.root {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapperButton {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
outline: initial;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 5px;
|
|
||||||
}
|
|
|
@ -1,10 +1,8 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Edit2, Tag, Cpu } from 'lucide-react';
|
import { Tag, Globe, Activity } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isoDateFromTimestamp,
|
isoDateFromTimestamp,
|
||||||
humanize,
|
|
||||||
stripProtocol,
|
stripProtocol,
|
||||||
} from '@/portainer/filters/filters';
|
} from '@/portainer/filters/filters';
|
||||||
import {
|
import {
|
||||||
|
@ -12,24 +10,22 @@ import {
|
||||||
PlatformType,
|
PlatformType,
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import {
|
import {
|
||||||
|
getDashboardRoute,
|
||||||
getPlatformType,
|
getPlatformType,
|
||||||
isDockerEnvironment,
|
|
||||||
isEdgeEnvironment,
|
isEdgeEnvironment,
|
||||||
} from '@/react/portainer/environments/utils';
|
} from '@/react/portainer/environments/utils';
|
||||||
import type { TagId } from '@/portainer/tags/types';
|
import type { TagId } from '@/portainer/tags/types';
|
||||||
import { useTags } from '@/portainer/tags/queries';
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
|
||||||
import Memory from '@/assets/ico/memory.svg?c';
|
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
|
||||||
import { Link } from '@@/Link';
|
|
||||||
import { Button } from '@@/buttons';
|
|
||||||
import { EdgeIndicator } from '@@/EdgeIndicator';
|
import { EdgeIndicator } from '@@/EdgeIndicator';
|
||||||
|
import { EnvironmentStatusBadge } from '@@/EnvironmentStatusBadge';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { EnvironmentIcon } from './EnvironmentIcon';
|
import { EnvironmentIcon } from './EnvironmentIcon';
|
||||||
import { EnvironmentStats } from './EnvironmentStats';
|
import { EnvironmentStats } from './EnvironmentStats';
|
||||||
import styles from './EnvironmentItem.module.css';
|
import { EngineVersion } from './EngineVersion';
|
||||||
import { EnvironmentStatusBadge } from './EnvironmentStatusBadge';
|
import { AgentVersionTag } from './AgentVersionTag';
|
||||||
|
import { EditButtons } from './EditButtons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
|
@ -38,118 +34,102 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
||||||
const { isAdmin } = useUser();
|
|
||||||
const isEdge = isEdgeEnvironment(environment.Type);
|
const isEdge = isEdgeEnvironment(environment.Type);
|
||||||
|
|
||||||
const snapshotTime = getSnapshotTime(environment);
|
const snapshotTime = getSnapshotTime(environment);
|
||||||
|
|
||||||
const tags = useEnvironmentTagNames(environment.TagIds);
|
const tags = useEnvironmentTagNames(environment.TagIds);
|
||||||
const route = getRoute(environment);
|
const route = getDashboardRoute(environment);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onClick(environment)}
|
onClick={() => onClick(environment)}
|
||||||
className={styles.wrapperButton}
|
className="bg-transparent border-0 !p-0 !m-0"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
className={clsx('blocklist-item no-link', styles.item)}
|
className="blocklist-item flex no-link overflow-hidden min-h-[100px]"
|
||||||
to={route}
|
to={route}
|
||||||
params={{
|
params={{
|
||||||
endpointId: environment.Id,
|
endpointId: environment.Id,
|
||||||
id: environment.Id,
|
id: environment.Id,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="blocklist-item-box">
|
<div className="ml-2 self-center flex justify-center">
|
||||||
<span className={clsx('blocklist-item-logo', 'endpoint-item')}>
|
|
||||||
<EnvironmentIcon type={environment.Type} />
|
<EnvironmentIcon type={environment.Type} />
|
||||||
</span>
|
</div>
|
||||||
<span className="col-sm-12">
|
<div className="ml-3 mr-auto flex justify-center gap-3 flex-col items-start">
|
||||||
<div className="blocklist-item-line endpoint-item">
|
<div className="space-x-3 flex items-center">
|
||||||
<span>
|
<span className="font-bold">{environment.Name}</span>
|
||||||
<span className="blocklist-item-title endpoint-item">
|
|
||||||
{environment.Name}
|
|
||||||
</span>
|
|
||||||
<span className="space-left blocklist-item-subtitle">
|
|
||||||
{isEdge ? (
|
{isEdge ? (
|
||||||
<EdgeIndicator
|
<EdgeIndicator environment={environment} showLastCheckInDate />
|
||||||
environment={environment}
|
|
||||||
showLastCheckInDate
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<EnvironmentStatusBadge status={environment.Status} />
|
<EnvironmentStatusBadge status={environment.Status} />
|
||||||
<span className="space-left small text-muted">
|
{snapshotTime && (
|
||||||
|
<span
|
||||||
|
className="space-left small text-muted vertical-center"
|
||||||
|
title="Last snapshot time"
|
||||||
|
>
|
||||||
|
<Activity
|
||||||
|
className="icon icon-sm space-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{snapshotTime}
|
{snapshotTime}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
|
||||||
</span>
|
<EngineVersion environment={environment} />
|
||||||
{groupName && (
|
|
||||||
<span className="small space-right">
|
|
||||||
<span>Group: </span>
|
|
||||||
<span>{groupName}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<EnvironmentStats environment={environment} />
|
|
||||||
<div className="blocklist-item-line endpoint-item">
|
|
||||||
<span className="small text-muted space-x-2">
|
|
||||||
{isDockerEnvironment(environment.Type) && (
|
|
||||||
<span>
|
|
||||||
{environment.Snapshots.length > 0 && (
|
|
||||||
<span className="small text-muted vertical-center">
|
|
||||||
<Cpu
|
|
||||||
className="icon icon-sm space-right"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{environment.Snapshots[0].TotalCPU} CPU
|
|
||||||
<Icon
|
|
||||||
icon={Memory}
|
|
||||||
className="icon icon-sm space-right"
|
|
||||||
/>
|
|
||||||
{humanize(environment.Snapshots[0].TotalMemory)} RAM
|
|
||||||
<Cpu
|
|
||||||
className="icon icon-sm space-right"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{environment.Gpus?.length} GPU
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="vertical-center">
|
|
||||||
<Tag
|
|
||||||
className="icon icon-sm space-right"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{tags}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{!isEdge && (
|
{!isEdge && (
|
||||||
<span className="small text-muted">
|
<span className="text-muted small vertical-center">
|
||||||
{stripProtocol(environment.URL)}
|
{stripProtocol(environment.URL)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="small text-muted space-x-2 vertical-center">
|
||||||
|
{groupName && (
|
||||||
|
<span className="font-semibold">
|
||||||
|
<span>Group: </span>
|
||||||
|
<span>{groupName}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
</Link>
|
|
||||||
</button>
|
<span className="vertical-center">
|
||||||
{isAdmin && (
|
<Tag className="icon icon-sm space-right" aria-hidden="true" />
|
||||||
<Link
|
{tags}
|
||||||
to="portainer.endpoints.endpoint"
|
</span>
|
||||||
params={{ id: environment.Id }}
|
|
||||||
className={styles.editButton}
|
{isEdge && (
|
||||||
>
|
<>
|
||||||
<Button color="link">
|
<AgentVersionTag
|
||||||
<Edit2 className="icon icon-md" aria-hidden="true" />
|
type={environment.Type}
|
||||||
</Button>
|
version={environment.Agent.Version}
|
||||||
</Link>
|
/>
|
||||||
|
|
||||||
|
{environment.Edge.AsyncMode && (
|
||||||
|
<span className="vertical-center gap-1">
|
||||||
|
<Globe
|
||||||
|
className="icon icon-sm space-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Async Environment
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EnvironmentStats environment={environment} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditButtons environment={environment} />
|
||||||
|
</Link>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,22 +173,3 @@ function getSnapshotTime(environment: Environment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoute(environment: Environment) {
|
|
||||||
if (isEdgeEnvironment(environment.Type) && !environment.EdgeID) {
|
|
||||||
return 'portainer.endpoints.endpoint';
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = getPlatformType(environment.Type);
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case PlatformType.Azure:
|
|
||||||
return 'azure.dashboard';
|
|
||||||
case PlatformType.Docker:
|
|
||||||
return 'docker.dashboard';
|
|
||||||
case PlatformType.Kubernetes:
|
|
||||||
return 'kubernetes.dashboard';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { getPlatformType } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
import { EnvironmentStatsDocker } from './EnvironmentStatsDocker';
|
import { EnvironmentStatsDocker } from './EnvironmentStatsDocker';
|
||||||
import { EnvironmentStatsKubernetes } from './EnvironmentStatsKubernetes';
|
import { EnvironmentStatsKubernetes } from './EnvironmentStatsKubernetes';
|
||||||
|
import { EnvironmentStatsNomad } from './EnvironmentStatsNomad';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
|
@ -13,28 +14,31 @@ interface Props {
|
||||||
|
|
||||||
export function EnvironmentStats({ environment }: Props) {
|
export function EnvironmentStats({ environment }: Props) {
|
||||||
const platform = getPlatformType(environment.Type);
|
const platform = getPlatformType(environment.Type);
|
||||||
|
|
||||||
|
const component = getComponent(platform, environment);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="blocklist-item-desc flex items-center gap-10">
|
||||||
|
{component}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getComponent(platform: PlatformType, environment: Environment) {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case PlatformType.Kubernetes:
|
case PlatformType.Kubernetes:
|
||||||
return (
|
return (
|
||||||
<EnvironmentStatsKubernetes
|
<EnvironmentStatsKubernetes
|
||||||
snapshots={environment.Kubernetes.Snapshots || []}
|
snapshot={environment.Kubernetes.Snapshots?.[0]}
|
||||||
type={environment.Type}
|
|
||||||
agentVersion={environment.Agent.Version}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case PlatformType.Docker:
|
case PlatformType.Docker:
|
||||||
|
return <EnvironmentStatsDocker snapshot={environment.Snapshots?.[0]} />;
|
||||||
|
case PlatformType.Nomad:
|
||||||
return (
|
return (
|
||||||
<EnvironmentStatsDocker
|
<EnvironmentStatsNomad snapshot={environment.Nomad.Snapshots?.[0]} />
|
||||||
snapshots={environment.Snapshots}
|
|
||||||
type={environment.Type}
|
|
||||||
agentVersion={environment.Agent.Version}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return null;
|
||||||
<div className="blocklist-item-line endpoint-item">
|
|
||||||
<span className="blocklist-item-desc">-</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,46 +9,29 @@ import {
|
||||||
Heart,
|
Heart,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import {
|
|
||||||
DockerSnapshot,
|
|
||||||
EnvironmentType,
|
|
||||||
} from '@/react/portainer/environments/types';
|
|
||||||
import { addPlural } from '@/portainer/helpers/strings';
|
import { addPlural } from '@/portainer/helpers/strings';
|
||||||
|
import { DockerSnapshot } from '@/react/docker/snapshots/types';
|
||||||
|
|
||||||
import { AgentVersionTag } from './AgentVersionTag';
|
import { StatsItem } from '@@/StatsItem';
|
||||||
import { EnvironmentStatsItem } from './EnvironmentStatsItem';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshots: DockerSnapshot[];
|
snapshot?: DockerSnapshot;
|
||||||
type: EnvironmentType;
|
|
||||||
agentVersion: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EnvironmentStatsDocker({
|
export function EnvironmentStatsDocker({ snapshot }: Props) {
|
||||||
snapshots = [],
|
if (!snapshot) {
|
||||||
type,
|
return <>No snapshot available</>;
|
||||||
agentVersion,
|
|
||||||
}: Props) {
|
|
||||||
if (snapshots.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="blocklist-item-line endpoint-item">
|
|
||||||
<span className="blocklist-item-desc">No snapshot available</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = snapshots[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="blocklist-item-line endpoint-item">
|
<>
|
||||||
<span className="blocklist-item-desc">
|
<StatsItem
|
||||||
<EnvironmentStatsItem
|
|
||||||
value={addPlural(snapshot.StackCount, 'stack')}
|
value={addPlural(snapshot.StackCount, 'stack')}
|
||||||
icon={Layers}
|
icon={Layers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!!snapshot.Swarm && (
|
{!!snapshot.Swarm && (
|
||||||
<EnvironmentStatsItem
|
<StatsItem
|
||||||
value={addPlural(snapshot.ServiceCount, 'service')}
|
value={addPlural(snapshot.ServiceCount, 'service')}
|
||||||
icon={Shuffle}
|
icon={Shuffle}
|
||||||
/>
|
/>
|
||||||
|
@ -60,29 +43,19 @@ export function EnvironmentStatsDocker({
|
||||||
healthy={snapshot.HealthyContainerCount}
|
healthy={snapshot.HealthyContainerCount}
|
||||||
unhealthy={snapshot.UnhealthyContainerCount}
|
unhealthy={snapshot.UnhealthyContainerCount}
|
||||||
/>
|
/>
|
||||||
<EnvironmentStatsItem
|
<StatsItem
|
||||||
value={addPlural(snapshot.VolumeCount, 'volume')}
|
value={addPlural(snapshot.VolumeCount, 'volume')}
|
||||||
icon={Database}
|
icon={Database}
|
||||||
/>
|
/>
|
||||||
<EnvironmentStatsItem
|
<StatsItem value={addPlural(snapshot.ImageCount, 'image')} icon={List} />
|
||||||
value={addPlural(snapshot.ImageCount, 'image')}
|
|
||||||
icon={List}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="small text-muted space-x-2 vertical-center">
|
|
||||||
<span>
|
|
||||||
{snapshot.Swarm ? 'Swarm' : 'Standalone'} {snapshot.DockerVersion}
|
|
||||||
</span>
|
|
||||||
{snapshot.Swarm && (
|
{snapshot.Swarm && (
|
||||||
<EnvironmentStatsItem
|
<StatsItem
|
||||||
value={addPlural(snapshot.NodeCount, 'node')}
|
value={addPlural(snapshot.NodeCount, 'node')}
|
||||||
icon={HardDrive}
|
icon={HardDrive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AgentVersionTag version={agentVersion} type={type} />
|
</>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,34 +75,15 @@ function ContainerStats({
|
||||||
const containersCount = running + stopped;
|
const containersCount = running + stopped;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnvironmentStatsItem
|
<StatsItem value={addPlural(containersCount, 'container')} icon={Box}>
|
||||||
value={addPlural(containersCount, 'container')}
|
|
||||||
icon={Box}
|
|
||||||
>
|
|
||||||
{containersCount > 0 && (
|
{containersCount > 0 && (
|
||||||
<span className="space-x-2 space-right">
|
<>
|
||||||
<EnvironmentStatsItem
|
<StatsItem value={running} icon={Power} iconClass="icon-success" />
|
||||||
value={running}
|
<StatsItem value={stopped} icon={Power} iconClass="icon-danger" />
|
||||||
icon={Power}
|
<StatsItem value={healthy} icon={Heart} iconClass="icon-success" />
|
||||||
iconClass="icon-success"
|
<StatsItem value={unhealthy} icon={Heart} iconClass="icon-warning" />
|
||||||
/>
|
</>
|
||||||
<EnvironmentStatsItem
|
|
||||||
value={stopped}
|
|
||||||
icon={Power}
|
|
||||||
iconClass="icon-danger"
|
|
||||||
/>
|
|
||||||
<EnvironmentStatsItem
|
|
||||||
value={healthy}
|
|
||||||
icon={Heart}
|
|
||||||
iconClass="icon-success"
|
|
||||||
/>
|
|
||||||
<EnvironmentStatsItem
|
|
||||||
value={unhealthy}
|
|
||||||
icon={Heart}
|
|
||||||
iconClass="icon-warning"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</EnvironmentStatsItem>
|
</StatsItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,34 @@
|
||||||
import { Cpu, HardDrive } from 'lucide-react';
|
import { Cpu, HardDrive } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import { KubernetesSnapshot } from '@/react/portainer/environments/types';
|
||||||
EnvironmentType,
|
|
||||||
KubernetesSnapshot,
|
|
||||||
} from '@/react/portainer/environments/types';
|
|
||||||
import { humanize } from '@/portainer/filters/filters';
|
import { humanize } from '@/portainer/filters/filters';
|
||||||
import { addPlural } from '@/portainer/helpers/strings';
|
import { addPlural } from '@/portainer/helpers/strings';
|
||||||
import Memory from '@/assets/ico/memory.svg?c';
|
import Memory from '@/assets/ico/memory.svg?c';
|
||||||
|
|
||||||
import { AgentVersionTag } from './AgentVersionTag';
|
import { StatsItem } from '@@/StatsItem';
|
||||||
import { EnvironmentStatsItem } from './EnvironmentStatsItem';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshots?: KubernetesSnapshot[];
|
snapshot?: KubernetesSnapshot;
|
||||||
type: EnvironmentType;
|
|
||||||
agentVersion: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EnvironmentStatsKubernetes({
|
export function EnvironmentStatsKubernetes({ snapshot }: Props) {
|
||||||
snapshots = [],
|
if (!snapshot) {
|
||||||
type,
|
return <>No snapshot available</>;
|
||||||
agentVersion,
|
|
||||||
}: Props) {
|
|
||||||
if (snapshots.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="blocklist-item-line endpoint-item">
|
|
||||||
<span className="blocklist-item-desc">No snapshot available</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = snapshots[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="blocklist-item-line endpoint-item">
|
<>
|
||||||
<span className="blocklist-item-desc space-x-1">
|
<StatsItem icon={Cpu} value={`${snapshot.TotalCPU} CPU`} />
|
||||||
<EnvironmentStatsItem icon={Cpu} value={`${snapshot.TotalCPU} CPU`} />
|
|
||||||
|
|
||||||
<EnvironmentStatsItem
|
<StatsItem
|
||||||
icon={Memory}
|
icon={Memory}
|
||||||
value={`${humanize(snapshot.TotalMemory)} RAM`}
|
value={`${humanize(snapshot.TotalMemory)} RAM`}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="small text-muted space-x-2 vertical-center">
|
<StatsItem
|
||||||
<span>Kubernetes {snapshot.KubernetesVersion}</span>
|
|
||||||
<EnvironmentStatsItem
|
|
||||||
value={addPlural(snapshot.NodeCount, 'node')}
|
value={addPlural(snapshot.NodeCount, 'node')}
|
||||||
icon={HardDrive}
|
icon={HardDrive}
|
||||||
/>
|
/>
|
||||||
<AgentVersionTag type={type} version={agentVersion} />
|
</>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Box, Dice4, HardDrive, List, Power } from 'lucide-react';
|
||||||
|
|
||||||
|
import { NomadSnapshot } from '@/react/portainer/environments/types';
|
||||||
|
import { addPlural } from '@/portainer/helpers/strings';
|
||||||
|
|
||||||
|
import { StatsItem } from '@@/StatsItem';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
snapshot?: NomadSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnvironmentStatsNomad({ snapshot }: Props) {
|
||||||
|
if (!snapshot) {
|
||||||
|
return <>No snapshot available</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StatsItem value={addPlural(snapshot.JobCount, 'job')} icon={List} />
|
||||||
|
<StatsItem value={addPlural(snapshot.GroupCount, 'group')} icon={Dice4} />
|
||||||
|
<StatsItem value={addPlural(snapshot.TaskCount, 'task')} icon={Box}>
|
||||||
|
{snapshot.TaskCount > 0 && (
|
||||||
|
<>
|
||||||
|
<StatsItem
|
||||||
|
value={snapshot.RunningTaskCount}
|
||||||
|
icon={Power}
|
||||||
|
iconClass="icon-success"
|
||||||
|
/>
|
||||||
|
<StatsItem
|
||||||
|
value={snapshot.TaskCount - snapshot.RunningTaskCount}
|
||||||
|
icon={Power}
|
||||||
|
iconClass="icon-danger"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StatsItem>
|
||||||
|
|
||||||
|
<StatsItem
|
||||||
|
value={addPlural(snapshot.NodeCount, 'node')}
|
||||||
|
icon={HardDrive}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
status: EnvironmentStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnvironmentStatusBadge({ status }: Props) {
|
|
||||||
return (
|
|
||||||
<span className={clsx('label', `label-${environmentStatusBadge(status)}`)}>
|
|
||||||
{status === EnvironmentStatus.Up ? 'up' : 'down'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function environmentStatusBadge(status: EnvironmentStatus) {
|
|
||||||
if (status === EnvironmentStatus.Down) {
|
|
||||||
return 'danger';
|
|
||||||
}
|
|
||||||
return 'success';
|
|
||||||
}
|
|
|
@ -69,7 +69,6 @@ const storageKey = 'home_endpoints';
|
||||||
|
|
||||||
export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
||||||
const { isAdmin } = useUser();
|
const { isAdmin } = useUser();
|
||||||
|
|
||||||
const [platformTypes, setPlatformTypes] = useHomePageFilter<
|
const [platformTypes, setPlatformTypes] = useHomePageFilter<
|
||||||
Filter<PlatformType>[]
|
Filter<PlatformType>[]
|
||||||
>('platformType', []);
|
>('platformType', []);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||||
import { UserId } from '@/portainer/users/types';
|
import { UserId } from '@/portainer/users/types/user-id';
|
||||||
|
|
||||||
export type ResourceControlId = number;
|
export type ResourceControlId = number;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
||||||
|
import { Job } from '@/react/nomad/types';
|
||||||
|
import { DockerSnapshot } from '@/react/docker/snapshots/types';
|
||||||
|
|
||||||
export type EnvironmentId = number;
|
export type EnvironmentId = number;
|
||||||
|
|
||||||
|
@ -25,30 +27,14 @@ export enum EnvironmentType {
|
||||||
export const EdgeTypes = [
|
export const EdgeTypes = [
|
||||||
EnvironmentType.EdgeAgentOnDocker,
|
EnvironmentType.EdgeAgentOnDocker,
|
||||||
EnvironmentType.EdgeAgentOnKubernetes,
|
EnvironmentType.EdgeAgentOnKubernetes,
|
||||||
|
EnvironmentType.EdgeAgentOnNomad,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export enum EnvironmentStatus {
|
export enum EnvironmentStatus {
|
||||||
Up = 1,
|
Up = 1,
|
||||||
Down,
|
Down,
|
||||||
}
|
Provisioning,
|
||||||
|
Error,
|
||||||
export interface DockerSnapshot {
|
|
||||||
TotalCPU: number;
|
|
||||||
TotalMemory: number;
|
|
||||||
NodeCount: number;
|
|
||||||
ImageCount: number;
|
|
||||||
VolumeCount: number;
|
|
||||||
RunningContainerCount: number;
|
|
||||||
StoppedContainerCount: number;
|
|
||||||
HealthyContainerCount: number;
|
|
||||||
UnhealthyContainerCount: number;
|
|
||||||
Time: number;
|
|
||||||
StackCount: number;
|
|
||||||
ServiceCount: number;
|
|
||||||
Swarm: boolean;
|
|
||||||
DockerVersion: string;
|
|
||||||
GpuUseAll: boolean;
|
|
||||||
GpuUseList: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubernetesSnapshot {
|
export interface KubernetesSnapshot {
|
||||||
|
@ -80,6 +66,20 @@ export interface KubernetesSettings {
|
||||||
Configuration: KubernetesConfiguration;
|
Configuration: KubernetesConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NomadSnapshot {
|
||||||
|
JobCount: number;
|
||||||
|
GroupCount: number;
|
||||||
|
TaskCount: number;
|
||||||
|
RunningTaskCount: number;
|
||||||
|
NodeCount: number;
|
||||||
|
Time: number;
|
||||||
|
Jobs: Job[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NomadSettings {
|
||||||
|
Snapshots: NomadSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
export type EnvironmentEdge = {
|
export type EnvironmentEdge = {
|
||||||
AsyncMode: boolean;
|
AsyncMode: boolean;
|
||||||
PingInterval: number;
|
PingInterval: number;
|
||||||
|
@ -124,6 +124,7 @@ export type Environment = {
|
||||||
URL: string;
|
URL: string;
|
||||||
Snapshots: DockerSnapshot[];
|
Snapshots: DockerSnapshot[];
|
||||||
Kubernetes: KubernetesSettings;
|
Kubernetes: KubernetesSettings;
|
||||||
|
Nomad: NomadSettings;
|
||||||
PublicURL?: string;
|
PublicURL?: string;
|
||||||
IsEdgeDevice?: boolean;
|
IsEdgeDevice?: boolean;
|
||||||
UserTrusted: boolean;
|
UserTrusted: boolean;
|
||||||
|
|
|
@ -51,6 +51,10 @@ export function isEdgeEnvironment(envType: EnvironmentType) {
|
||||||
].includes(envType);
|
].includes(envType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEdgeAsync(env?: Environment | null) {
|
||||||
|
return !!env && env.Edge.AsyncMode;
|
||||||
|
}
|
||||||
|
|
||||||
export function isUnassociatedEdgeEnvironment(env: Environment) {
|
export function isUnassociatedEdgeEnvironment(env: Environment) {
|
||||||
return isEdgeEnvironment(env.Type) && !env.EdgeID;
|
return isEdgeEnvironment(env.Type) && !env.EdgeID;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue