mirror of https://github.com/portainer/portainer
feat(edge/stacks): ui for status [EE-5593] (#9214)
parent
03b9a9b65d
commit
4f0f53b9aa
|
@ -1,28 +0,0 @@
|
||||||
.root {
|
|
||||||
padding: 2px 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-acknowledged {
|
|
||||||
color: #337ab7;
|
|
||||||
background-color: rgba(51, 122, 183, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-images-pulled {
|
|
||||||
color: #e1a800;
|
|
||||||
background-color: rgba(238, 192, 32, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ok {
|
|
||||||
color: #23ae89;
|
|
||||||
background-color: rgba(35, 174, 137, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
|
||||||
color: #ae2323;
|
|
||||||
background-color: rgba(174, 35, 35, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-total {
|
|
||||||
background-color: rgba(168, 167, 167, 0.1);
|
|
||||||
}
|
|
|
@ -1,28 +1,30 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||||
|
|
||||||
import { EdgeStack, StatusType } from '../../types';
|
import { EdgeStack, StatusType } from '../../types';
|
||||||
|
|
||||||
import styles from './DeploymentCounter.module.css';
|
|
||||||
|
|
||||||
export function DeploymentCounterLink({
|
export function DeploymentCounterLink({
|
||||||
count,
|
count,
|
||||||
|
total,
|
||||||
type,
|
type,
|
||||||
stackId,
|
stackId,
|
||||||
}: {
|
}: {
|
||||||
count: number;
|
count: number;
|
||||||
|
total: number;
|
||||||
type: StatusType;
|
type: StatusType;
|
||||||
stackId: EdgeStack['Id'];
|
stackId: EdgeStack['Id'];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="w-full text-center">
|
||||||
<Link
|
<Link
|
||||||
className="hover:no-underline"
|
className="hover:no-underline"
|
||||||
to="edge.stacks.edit"
|
to="edge.stacks.edit"
|
||||||
params={{ stackId, tab: 1, status: type }}
|
params={{ stackId, tab: 1, status: type }}
|
||||||
>
|
>
|
||||||
<DeploymentCounter count={count} type={type} />
|
<DeploymentCounter count={count} type={type} total={total} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -30,22 +32,48 @@ export function DeploymentCounterLink({
|
||||||
|
|
||||||
export function DeploymentCounter({
|
export function DeploymentCounter({
|
||||||
count,
|
count,
|
||||||
|
total,
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
count: number;
|
count: number;
|
||||||
|
total: number;
|
||||||
type?: StatusType;
|
type?: StatusType;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span
|
<TooltipWithChildren message={getTooltip(count, total, type)}>
|
||||||
className={clsx(styles.root, {
|
<div className="h-2 w-full overflow-hidden rounded-lg bg-gray-4">
|
||||||
[styles.statusOk]: type === StatusType.Running,
|
<div
|
||||||
[styles.statusError]: type === StatusType.Error,
|
style={{ width: `${(count / total) * 100}%` }}
|
||||||
[styles.statusAcknowledged]: type === StatusType.Acknowledged,
|
className={clsx('h-full rounded-lg', {
|
||||||
[styles.statusImagesPulled]: type === StatusType.ImagesPulled,
|
'bg-success-7': type === StatusType.Running,
|
||||||
[styles.statusTotal]: type === undefined,
|
'bg-error-7': type === StatusType.Error,
|
||||||
})}
|
'bg-blue-9': type === StatusType.Acknowledged,
|
||||||
>
|
'bg-yellow-7': type === StatusType.ImagesPulled,
|
||||||
• {count}
|
})}
|
||||||
</span>
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipWithChildren>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTooltip(count: number, total: number, type?: StatusType) {
|
||||||
|
const label = getLabel(type);
|
||||||
|
return `${count} of ${total} ${label}`;
|
||||||
|
|
||||||
|
function getLabel(type?: StatusType): ReactNode {
|
||||||
|
switch (type) {
|
||||||
|
case StatusType.Running:
|
||||||
|
return 'deployments running';
|
||||||
|
case StatusType.DeploymentReceived:
|
||||||
|
return 'deployments received';
|
||||||
|
case StatusType.Error:
|
||||||
|
return 'deployments failed';
|
||||||
|
case StatusType.Acknowledged:
|
||||||
|
return 'deployments acknowledged';
|
||||||
|
case StatusType.ImagesPulled:
|
||||||
|
return 'images pre-pulled';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,15 +50,12 @@ export function EdgeStacksDatatable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function aggregateStackStatus(stackStatus: EdgeStack['Status']) {
|
function aggregateStackStatus(stackStatus: EdgeStack['Status']) {
|
||||||
const aggregateStatus = { ok: 0, error: 0, acknowledged: 0, imagesPulled: 0 };
|
const aggregateStatus: Partial<Record<StatusType, number>> = {};
|
||||||
return Object.values(stackStatus).reduce(
|
return Object.values(stackStatus).reduce(
|
||||||
(acc, envStatus) =>
|
(acc, envStatus) =>
|
||||||
envStatus.Status.reduce((acc, status) => {
|
envStatus.Status.reduce((acc, status) => {
|
||||||
const { Type } = status;
|
const { Type } = status;
|
||||||
acc.ok += Number(Type === StatusType.Running);
|
acc[Type] = (acc[Type] || 0) + 1;
|
||||||
acc.error += Number(Type === StatusType.Error);
|
|
||||||
acc.acknowledged += Number(Type === StatusType.Acknowledged);
|
|
||||||
acc.imagesPulled += Number(Type === StatusType.ImagesPulled);
|
|
||||||
return acc;
|
return acc;
|
||||||
}, acc),
|
}, acc),
|
||||||
aggregateStatus
|
aggregateStatus
|
||||||
|
|
|
@ -5,12 +5,13 @@ import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { StatusType } from '../../types';
|
import { StatusType } from '../../types';
|
||||||
|
|
||||||
import { EdgeStackStatus } from './EdgeStacksStatus';
|
import { EdgeStackStatus } from './EdgeStacksStatus';
|
||||||
import { DecoratedEdgeStack } from './types';
|
import { DecoratedEdgeStack } from './types';
|
||||||
import { DeploymentCounter, DeploymentCounterLink } from './DeploymentCounter';
|
import { DeploymentCounter } from './DeploymentCounter';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<DecoratedEdgeStack>();
|
const columnHelper = createColumnHelper<DecoratedEdgeStack>();
|
||||||
|
|
||||||
|
@ -21,29 +22,52 @@ export const columns = _.compact([
|
||||||
'edge.stacks.edit',
|
'edge.stacks.edit',
|
||||||
'stackId'
|
'stackId'
|
||||||
),
|
),
|
||||||
columnHelper.accessor('aggregatedStatus.acknowledged', {
|
columnHelper.accessor(
|
||||||
header: 'Acknowledged',
|
(item) => item.aggregatedStatus[StatusType.Acknowledged] || 0,
|
||||||
enableSorting: false,
|
{
|
||||||
enableHiding: false,
|
header: 'Acknowledged',
|
||||||
cell: ({ getValue, row }) => (
|
enableSorting: false,
|
||||||
<DeploymentCounterLink
|
enableHiding: false,
|
||||||
count={getValue()}
|
|
||||||
type={StatusType.Acknowledged}
|
|
||||||
stackId={row.original.Id}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
meta: {
|
|
||||||
className: '[&>*]:justify-center',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
isBE &&
|
|
||||||
columnHelper.accessor('aggregatedStatus.imagesPulled', {
|
|
||||||
header: 'Images Pre-pulled',
|
|
||||||
cell: ({ getValue, row }) => (
|
cell: ({ getValue, row }) => (
|
||||||
<DeploymentCounterLink
|
<DeploymentCounter
|
||||||
count={getValue()}
|
count={getValue()}
|
||||||
type={StatusType.ImagesPulled}
|
type={StatusType.Acknowledged}
|
||||||
stackId={row.original.Id}
|
total={row.original.NumDeployments}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
className: '[&>*]:justify-center',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
isBE &&
|
||||||
|
columnHelper.accessor(
|
||||||
|
(item) => item.aggregatedStatus[StatusType.ImagesPulled] || 0,
|
||||||
|
{
|
||||||
|
header: 'Images pre-pulled',
|
||||||
|
cell: ({ getValue, row }) => (
|
||||||
|
<DeploymentCounter
|
||||||
|
count={getValue()}
|
||||||
|
type={StatusType.ImagesPulled}
|
||||||
|
total={row.original.NumDeployments}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
className: '[&>*]:justify-center',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
columnHelper.accessor(
|
||||||
|
(item) => item.aggregatedStatus[StatusType.DeploymentReceived] || 0,
|
||||||
|
{
|
||||||
|
header: 'Deployments received',
|
||||||
|
cell: ({ getValue, row }) => (
|
||||||
|
<DeploymentCounter
|
||||||
|
count={getValue()}
|
||||||
|
type={StatusType.Running}
|
||||||
|
total={row.original.NumDeployments}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
|
@ -51,37 +75,45 @@ export const columns = _.compact([
|
||||||
meta: {
|
meta: {
|
||||||
className: '[&>*]:justify-center',
|
className: '[&>*]:justify-center',
|
||||||
},
|
},
|
||||||
}),
|
}
|
||||||
columnHelper.accessor('aggregatedStatus.ok', {
|
),
|
||||||
header: 'Deployed',
|
columnHelper.accessor(
|
||||||
cell: ({ getValue, row }) => (
|
(item) => item.aggregatedStatus[StatusType.Error] || 0,
|
||||||
<DeploymentCounterLink
|
{
|
||||||
count={getValue()}
|
header: 'Deployments failed',
|
||||||
type={StatusType.Running}
|
cell: ({ getValue, row }) => {
|
||||||
stackId={row.original.Id}
|
const count = getValue();
|
||||||
/>
|
|
||||||
),
|
return (
|
||||||
enableSorting: false,
|
<div className="flex items-center gap-2">
|
||||||
enableHiding: false,
|
<DeploymentCounter
|
||||||
meta: {
|
count={count}
|
||||||
className: '[&>*]:justify-center',
|
type={StatusType.Error}
|
||||||
},
|
total={row.original.NumDeployments}
|
||||||
}),
|
/>
|
||||||
columnHelper.accessor('aggregatedStatus.error', {
|
{count > 0 && (
|
||||||
header: 'Failed',
|
<Link
|
||||||
cell: ({ getValue, row }) => (
|
className="hover:no-underline"
|
||||||
<DeploymentCounterLink
|
to="edge.stacks.edit"
|
||||||
count={getValue()}
|
params={{
|
||||||
type={StatusType.Error}
|
stackId: row.original.Id,
|
||||||
stackId={row.original.Id}
|
tab: 1,
|
||||||
/>
|
status: StatusType.Error,
|
||||||
),
|
}}
|
||||||
enableSorting: false,
|
>
|
||||||
enableHiding: false,
|
({count}/{row.original.NumDeployments})
|
||||||
meta: {
|
</Link>
|
||||||
className: '[&>*]:justify-center',
|
)}
|
||||||
},
|
</div>
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
className: '[&>*]:justify-center',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
columnHelper.accessor('Status', {
|
columnHelper.accessor('Status', {
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
|
@ -95,19 +127,6 @@ export const columns = _.compact([
|
||||||
className: '[&>*]:justify-center',
|
className: '[&>*]:justify-center',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('NumDeployments', {
|
|
||||||
header: 'Deployments',
|
|
||||||
cell: ({ getValue }) => (
|
|
||||||
<div className="text-center">
|
|
||||||
<DeploymentCounter count={getValue()} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
meta: {
|
|
||||||
className: '[&>*]:justify-center',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('CreationDate', {
|
columnHelper.accessor('CreationDate', {
|
||||||
header: 'Creation Date',
|
header: 'Creation Date',
|
||||||
cell: ({ getValue }) => isoDateFromTimestamp(getValue()),
|
cell: ({ getValue }) => isoDateFromTimestamp(getValue()),
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import { EdgeStack } from '../../types';
|
import { EdgeStack, StatusType } from '../../types';
|
||||||
|
|
||||||
interface AggregateStackStatus {
|
|
||||||
ok: number;
|
|
||||||
error: number;
|
|
||||||
acknowledged: number;
|
|
||||||
imagesPulled: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DecoratedEdgeStack = EdgeStack & {
|
export type DecoratedEdgeStack = EdgeStack & {
|
||||||
aggregatedStatus: AggregateStackStatus;
|
aggregatedStatus: Partial<Record<StatusType, number>>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue