mirror of https://github.com/portainer/portainer
feat(home): change layout of env tile [EE-4479] (#8061)
parent
b48aa1274d
commit
eba5879ec8
@ -0,0 +1 @@
|
||||
export type UserId = number;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,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';
|
||||
}
|
Loading…
Reference in new issue