feat(home): change layout of env tile [EE-4479] (#8061)

pull/8180/head
Chaim Lev-Ari 2 years ago committed by GitHub
parent b48aa1274d
commit eba5879ec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -205,7 +205,6 @@ input[type='checkbox'] {
.blocklist-item {
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
border: 1px solid var(--border-blocklist);
border-radius: 8px;
margin-right: 10px;
@ -222,8 +221,9 @@ input[type='checkbox'] {
color: var(--text-blocklist-item-selected-color);
}
.blocklist-item:hover {
.blocklist-item:not(.blocklist-item-not-interactive):hover {
@apply border border-blue-7;
cursor: pointer;
background-color: var(--bg-blocklist-hover-color);
color: var(--text-blocklist-hover-color);
@ -233,10 +233,6 @@ input[type='checkbox'] {
display: flex;
}
.blocklist-item-line.endpoint-item {
padding: 4px;
}
.blocklist-item-line {
display: flex;
justify-content: space-between;
@ -248,24 +244,11 @@ input[type='checkbox'] {
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 {
font-size: 1.8em;
font-weight: bold;
}
.blocklist-item-title.endpoint-item {
font-size: 1em;
font-weight: bold;
}
.blocklist-item-subtitle {
font-size: 0.9em;
padding-right: 1em;
@ -660,7 +643,7 @@ input[type='checkbox'] {
margin-right: -50%;
}
/*bootbox override*/
/*bootbox override */
.modal-open {
padding-right: 0 !important;
}

@ -4,7 +4,7 @@
border-radius: 8px;
display: inline-flex;
justify-content: space-around;
justify-content: center;
align-items: center;
gap: 5px;
}

@ -1,6 +1,8 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
export type UserId = number;
import { UserId } from './types/user-id';
export { type UserId };
export enum Role {
Admin = 1,

@ -0,0 +1 @@
export type UserId = number;

@ -78,6 +78,7 @@ export function createMockEnvironment(): Environment {
AllowNoneIngressClass: false,
},
},
Nomad: { Snapshots: [] },
EdgeKey: '',
Id: 3,
UserTrusted: false,

@ -1,9 +1,10 @@
import clsx from 'clsx';
import { Activity } from 'lucide-react';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat';
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 {
showLastCheckInDate?: boolean;
@ -15,100 +16,46 @@ export function EdgeIndicator({
showLastCheckInDate = false,
}: Props) {
const associated = !!environment.EdgeID;
const isValid = useHasHeartbeat(environment, associated);
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">
<EnvironmentStatusBadgeItem aria-label="unassociated">
<s>associated</s>
</span>
</EnvironmentStatusBadgeItem>
</span>
);
}
return (
<span role="status" aria-label="edge-status">
<span
className={clsx('label', {
'label-danger': !isValid,
'label-success': isValid,
})}
<span
role="status"
aria-label="edge-status"
className="flex items-center gap-1"
>
<EnvironmentStatusBadgeItem
color={isValid ? 'success' : 'danger'}
aria-label="edge-heartbeat"
>
heartbeat
</span>
</EnvironmentStatusBadgeItem>
{showLastCheckInDate && !!environment.LastCheckInDate && (
<span
className="space-left small text-muted"
className="small text-muted vertical-center"
aria-label="edge-last-checkin"
title="Last edge check-in"
>
<Activity className="icon icon-sm space-right" aria-hidden="true" />
{isoDateFromTimestamp(environment.LastCheckInDate)}
</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;
}
export function EnvironmentStatsItem({
export function StatsItem({
value,
icon,
children,
iconClass,
}: PropsWithChildren<Props>) {
return (
<span className="vertical-center space-right">
<Icon
className={clsx('icon icon-sm space-right', iconClass)}
icon={icon}
/>
<span className="flex gap-1 items-center">
<Icon className={clsx('icon icon-sm', iconClass)} icon={icon} />
<span>{value}</span>
{children && <span className="space-left">{children}</span>}
{children && (
<span className="ml-1 flex gap-2 items-center">{children}</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 clsx from 'clsx';
import { Environment } from '@/react/portainer/environments/types';
import { EdgeIndicator } from '@@/EdgeIndicator';
import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat';
export const heartbeat: Column<Environment> = {
Header: 'Heartbeat',
@ -18,3 +18,36 @@ export function StatusCell({
}: CellProps<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 { Icon } from '@@/Icon';
import { StatsItem } from '@@/StatsItem';
interface Props {
running: number;
@ -11,11 +11,19 @@ export function RunningStatus({ running, stopped }: Props) {
return (
<div>
<div>
<Icon icon={Power} mode="success" />
<StatsItem
value={`${running || '-'} running`}
icon={Power}
iconClass="icon-success"
/>
{`${running || '-'} running`}
</div>
<div>
<Icon icon={Power} mode="danger" />
<StatsItem
value={`${stopped || '-'} stopped`}
icon={Power}
iconClass="icon-danger"
/>
{`${stopped || '-'} stopped`}
</div>
</div>

@ -18,9 +18,8 @@ export function AgentVersionTag({ type, version }: Props) {
return (
<span className="space-x-1">
<span>
<Zap className="icon icon-xs vertical-center" aria-hidden="true" />
</span>
<Zap className="icon icon-xs vertical-center" aria-hidden="true" />
<span>{isEdgeEnvironment(type) ? 'Edge Agent' : 'Agent'}</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 { Edit2, Tag, Cpu } from 'lucide-react';
import { Tag, Globe, Activity } from 'lucide-react';
import {
isoDateFromTimestamp,
humanize,
stripProtocol,
} from '@/portainer/filters/filters';
import {
@ -12,24 +10,22 @@ import {
PlatformType,
} from '@/react/portainer/environments/types';
import {
getDashboardRoute,
getPlatformType,
isDockerEnvironment,
isEdgeEnvironment,
} from '@/react/portainer/environments/utils';
import type { TagId } from '@/portainer/tags/types';
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 { EnvironmentStatusBadge } from '@@/EnvironmentStatusBadge';
import { Link } from '@@/Link';
import { EnvironmentIcon } from './EnvironmentIcon';
import { EnvironmentStats } from './EnvironmentStats';
import styles from './EnvironmentItem.module.css';
import { EnvironmentStatusBadge } from './EnvironmentStatusBadge';
import { EngineVersion } from './EngineVersion';
import { AgentVersionTag } from './AgentVersionTag';
import { EditButtons } from './EditButtons';
interface Props {
environment: Environment;
@ -38,118 +34,102 @@ interface Props {
}
export function EnvironmentItem({ environment, onClick, groupName }: Props) {
const { isAdmin } = useUser();
const isEdge = isEdgeEnvironment(environment.Type);
const snapshotTime = getSnapshotTime(environment);
const tags = useEnvironmentTagNames(environment.TagIds);
const route = getRoute(environment);
const route = getDashboardRoute(environment);
return (
<div className={styles.root}>
<button
type="button"
onClick={() => onClick(environment)}
className={styles.wrapperButton}
<button
type="button"
onClick={() => onClick(environment)}
className="bg-transparent border-0 !p-0 !m-0"
>
<Link
className="blocklist-item flex no-link overflow-hidden min-h-[100px]"
to={route}
params={{
endpointId: environment.Id,
id: environment.Id,
}}
>
<Link
className={clsx('blocklist-item no-link', styles.item)}
to={route}
params={{
endpointId: environment.Id,
id: environment.Id,
}}
>
<div className="blocklist-item-box">
<span className={clsx('blocklist-item-logo', 'endpoint-item')}>
<EnvironmentIcon type={environment.Type} />
</span>
<span className="col-sm-12">
<div className="blocklist-item-line endpoint-item">
<span>
<span className="blocklist-item-title endpoint-item">
{environment.Name}
</span>
<span className="space-left blocklist-item-subtitle">
{isEdge ? (
<EdgeIndicator
environment={environment}
showLastCheckInDate
/>
) : (
<>
<EnvironmentStatusBadge status={environment.Status} />
<span className="space-left small text-muted">
{snapshotTime}
</span>
</>
)}
</span>
</span>
{groupName && (
<span className="small space-right">
<span>Group: </span>
<span>{groupName}</span>
<div className="ml-2 self-center flex justify-center">
<EnvironmentIcon type={environment.Type} />
</div>
<div className="ml-3 mr-auto flex justify-center gap-3 flex-col items-start">
<div className="space-x-3 flex items-center">
<span className="font-bold">{environment.Name}</span>
{isEdge ? (
<EdgeIndicator environment={environment} showLastCheckInDate />
) : (
<>
<EnvironmentStatusBadge status={environment.Status} />
{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}
</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
</>
)}
<EngineVersion environment={environment} />
{!isEdge && (
<span className="text-muted small vertical-center">
{stripProtocol(environment.URL)}
</span>
)}
</div>
<div className="small text-muted space-x-2 vertical-center">
{groupName && (
<span className="font-semibold">
<span>Group: </span>
<span>{groupName}</span>
</span>
)}
<span className="vertical-center">
<Tag className="icon icon-sm space-right" aria-hidden="true" />
{tags}
</span>
{isEdge && (
<>
<AgentVersionTag
type={environment.Type}
version={environment.Agent.Version}
/>
{environment.Edge.AsyncMode && (
<span className="vertical-center gap-1">
<Globe
className="icon icon-sm space-right"
aria-hidden="true"
/>
{tags}
</span>
</span>
{!isEdge && (
<span className="small text-muted">
{stripProtocol(environment.URL)}
Async Environment
</span>
)}
</div>
</span>
</>
)}
</div>
</Link>
</button>
{isAdmin && (
<Link
to="portainer.endpoints.endpoint"
params={{ id: environment.Id }}
className={styles.editButton}
>
<Button color="link">
<Edit2 className="icon icon-md" aria-hidden="true" />
</Button>
</Link>
)}
</div>
<EnvironmentStats environment={environment} />
</div>
<EditButtons environment={environment} />
</Link>
</button>
);
}
@ -193,22 +173,3 @@ function getSnapshotTime(environment: Environment) {
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 { EnvironmentStatsKubernetes } from './EnvironmentStatsKubernetes';
import { EnvironmentStatsNomad } from './EnvironmentStatsNomad';
interface Props {
environment: Environment;
@ -13,28 +14,31 @@ interface Props {
export function EnvironmentStats({ environment }: Props) {
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) {
case PlatformType.Kubernetes:
return (
<EnvironmentStatsKubernetes
snapshots={environment.Kubernetes.Snapshots || []}
type={environment.Type}
agentVersion={environment.Agent.Version}
snapshot={environment.Kubernetes.Snapshots?.[0]}
/>
);
case PlatformType.Docker:
return <EnvironmentStatsDocker snapshot={environment.Snapshots?.[0]} />;
case PlatformType.Nomad:
return (
<EnvironmentStatsDocker
snapshots={environment.Snapshots}
type={environment.Type}
agentVersion={environment.Agent.Version}
/>
<EnvironmentStatsNomad snapshot={environment.Nomad.Snapshots?.[0]} />
);
default:
return (
<div className="blocklist-item-line endpoint-item">
<span className="blocklist-item-desc">-</span>
</div>
);
return null;
}
}

@ -9,80 +9,53 @@ import {
Heart,
} from 'lucide-react';
import {
DockerSnapshot,
EnvironmentType,
} from '@/react/portainer/environments/types';
import { addPlural } from '@/portainer/helpers/strings';
import { DockerSnapshot } from '@/react/docker/snapshots/types';
import { AgentVersionTag } from './AgentVersionTag';
import { EnvironmentStatsItem } from './EnvironmentStatsItem';
import { StatsItem } from '@@/StatsItem';
interface Props {
snapshots: DockerSnapshot[];
type: EnvironmentType;
agentVersion: string;
snapshot?: DockerSnapshot;
}
export function EnvironmentStatsDocker({
snapshots = [],
type,
agentVersion,
}: Props) {
if (snapshots.length === 0) {
return (
<div className="blocklist-item-line endpoint-item">
<span className="blocklist-item-desc">No snapshot available</span>
</div>
);
export function EnvironmentStatsDocker({ snapshot }: Props) {
if (!snapshot) {
return <>No snapshot available</>;
}
const snapshot = snapshots[0];
return (
<div className="blocklist-item-line endpoint-item">
<span className="blocklist-item-desc">
<EnvironmentStatsItem
value={addPlural(snapshot.StackCount, 'stack')}
icon={Layers}
<>
<StatsItem
value={addPlural(snapshot.StackCount, 'stack')}
icon={Layers}
/>
{!!snapshot.Swarm && (
<StatsItem
value={addPlural(snapshot.ServiceCount, 'service')}
icon={Shuffle}
/>
)}
{!!snapshot.Swarm && (
<EnvironmentStatsItem
value={addPlural(snapshot.ServiceCount, 'service')}
icon={Shuffle}
/>
)}
<ContainerStats
running={snapshot.RunningContainerCount}
stopped={snapshot.StoppedContainerCount}
healthy={snapshot.HealthyContainerCount}
unhealthy={snapshot.UnhealthyContainerCount}
/>
<StatsItem
value={addPlural(snapshot.VolumeCount, 'volume')}
icon={Database}
/>
<StatsItem value={addPlural(snapshot.ImageCount, 'image')} icon={List} />
<ContainerStats
running={snapshot.RunningContainerCount}
stopped={snapshot.StoppedContainerCount}
healthy={snapshot.HealthyContainerCount}
unhealthy={snapshot.UnhealthyContainerCount}
{snapshot.Swarm && (
<StatsItem
value={addPlural(snapshot.NodeCount, 'node')}
icon={HardDrive}
/>
<EnvironmentStatsItem
value={addPlural(snapshot.VolumeCount, 'volume')}
icon={Database}
/>
<EnvironmentStatsItem
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 && (
<EnvironmentStatsItem
value={addPlural(snapshot.NodeCount, 'node')}
icon={HardDrive}
/>
)}
<AgentVersionTag version={agentVersion} type={type} />
</span>
</div>
)}
</>
);
}
@ -102,34 +75,15 @@ function ContainerStats({
const containersCount = running + stopped;
return (
<EnvironmentStatsItem
value={addPlural(containersCount, 'container')}
icon={Box}
>
<StatsItem value={addPlural(containersCount, 'container')} icon={Box}>
{containersCount > 0 && (
<span className="space-x-2 space-right">
<EnvironmentStatsItem
value={running}
icon={Power}
iconClass="icon-success"
/>
<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>
<>
<StatsItem value={running} icon={Power} iconClass="icon-success" />
<StatsItem value={stopped} icon={Power} iconClass="icon-danger" />
<StatsItem value={healthy} icon={Heart} iconClass="icon-success" />
<StatsItem value={unhealthy} icon={Heart} iconClass="icon-warning" />
</>
)}
</EnvironmentStatsItem>
</StatsItem>
);
}

@ -1,56 +1,34 @@
import { Cpu, HardDrive } from 'lucide-react';
import {
EnvironmentType,
KubernetesSnapshot,
} from '@/react/portainer/environments/types';
import { KubernetesSnapshot } from '@/react/portainer/environments/types';
import { humanize } from '@/portainer/filters/filters';
import { addPlural } from '@/portainer/helpers/strings';
import Memory from '@/assets/ico/memory.svg?c';
import { AgentVersionTag } from './AgentVersionTag';
import { EnvironmentStatsItem } from './EnvironmentStatsItem';
import { StatsItem } from '@@/StatsItem';
interface Props {
snapshots?: KubernetesSnapshot[];
type: EnvironmentType;
agentVersion: string;
snapshot?: KubernetesSnapshot;
}
export function EnvironmentStatsKubernetes({
snapshots = [],
type,
agentVersion,
}: Props) {
if (snapshots.length === 0) {
return (
<div className="blocklist-item-line endpoint-item">
<span className="blocklist-item-desc">No snapshot available</span>
</div>
);
export function EnvironmentStatsKubernetes({ snapshot }: Props) {
if (!snapshot) {
return <>No snapshot available</>;
}
const snapshot = snapshots[0];
return (
<div className="blocklist-item-line endpoint-item">
<span className="blocklist-item-desc space-x-1">
<EnvironmentStatsItem icon={Cpu} value={`${snapshot.TotalCPU} CPU`} />
<EnvironmentStatsItem
icon={Memory}
value={`${humanize(snapshot.TotalMemory)} RAM`}
/>
</span>
<span className="small text-muted space-x-2 vertical-center">
<span>Kubernetes {snapshot.KubernetesVersion}</span>
<EnvironmentStatsItem
value={addPlural(snapshot.NodeCount, 'node')}
icon={HardDrive}
/>
<AgentVersionTag type={type} version={agentVersion} />
</span>
</div>
<>
<StatsItem icon={Cpu} value={`${snapshot.TotalCPU} CPU`} />
<StatsItem
icon={Memory}
value={`${humanize(snapshot.TotalMemory)} RAM`}
/>
<StatsItem
value={addPlural(snapshot.NodeCount, 'node')}
icon={HardDrive}
/>
</>
);
}

@ -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) {
const { isAdmin } = useUser();
const [platformTypes, setPlatformTypes] = useHomePageFilter<
Filter<PlatformType>[]
>('platformType', []);

@ -1,5 +1,5 @@
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;

@ -1,5 +1,7 @@
import { TagId } from '@/portainer/tags/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;
@ -25,30 +27,14 @@ export enum EnvironmentType {
export const EdgeTypes = [
EnvironmentType.EdgeAgentOnDocker,
EnvironmentType.EdgeAgentOnKubernetes,
EnvironmentType.EdgeAgentOnNomad,
] as const;
export enum EnvironmentStatus {
Up = 1,
Down,
}
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[];
Provisioning,
Error,
}
export interface KubernetesSnapshot {
@ -80,6 +66,20 @@ export interface KubernetesSettings {
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 = {
AsyncMode: boolean;
PingInterval: number;
@ -124,6 +124,7 @@ export type Environment = {
URL: string;
Snapshots: DockerSnapshot[];
Kubernetes: KubernetesSettings;
Nomad: NomadSettings;
PublicURL?: string;
IsEdgeDevice?: boolean;
UserTrusted: boolean;

@ -51,6 +51,10 @@ export function isEdgeEnvironment(envType: EnvironmentType) {
].includes(envType);
}
export function isEdgeAsync(env?: Environment | null) {
return !!env && env.Edge.AsyncMode;
}
export function isUnassociatedEdgeEnvironment(env: Environment) {
return isEdgeEnvironment(env.Type) && !env.EdgeID;
}

Loading…
Cancel
Save