1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 13:55:21 +02:00

feat(edge/stacks): increase status transparency [EE-5554] (#9094)

This commit is contained in:
Chaim Lev-Ari 2023-07-13 23:55:52 +03:00 committed by GitHub
parent db61fb149b
commit 0bcb57568c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1305 additions and 316 deletions

View file

@ -0,0 +1,5 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="14" height="14" rx="7" fill="#039855"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4729 4.31086L6.29623 8.34169L5.1879 7.15753C4.98373 6.96503 4.6629 6.95336 4.42957 7.11669C4.20207 7.28586 4.1379 7.58336 4.2779 7.82253L5.5904 9.95753C5.71873 10.1559 5.9404 10.2784 6.19123 10.2784C6.4304 10.2784 6.6579 10.1559 6.78623 9.95753C6.99623 9.68336 11.0037 4.90586 11.0037 4.90586C11.5287 4.36919 10.8929 3.89669 10.4729 4.30503V4.31086Z" fill="#0BA5EC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1403 4.30503C11.0118 4.12405 10.7084 4.07605 10.4729 4.30503V4.31086L10.3899 4.39096L6.29623 8.34169L5.1879 7.15753C4.98373 6.96503 4.6629 6.95336 4.42957 7.11669C4.20207 7.28586 4.1379 7.58336 4.2779 7.82253L5.5904 9.95753C5.69222 10.1149 5.85279 10.2245 6.04006 10.2631C6.08883 10.2731 6.13941 10.2784 6.19123 10.2784C6.31863 10.2784 6.44272 10.2436 6.55028 10.1811C6.64463 10.1263 6.72626 10.0502 6.78623 9.95753C6.78962 9.9531 6.794 9.94751 6.79933 9.94078C6.85798 9.86677 7.03221 9.65571 7.27843 9.35943C8.07017 8.40673 9.60624 6.57293 10.4372 5.5816C10.7807 5.17172 11.0037 4.90586 11.0037 4.90586C11.0111 4.89829 11.0183 4.89074 11.0253 4.8832C11.0268 4.88147 11.0284 4.87975 11.03 4.87803C11.2318 4.65583 11.2369 4.44719 11.1444 4.31086C11.143 4.3089 11.1417 4.30696 11.1403 4.30503Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,11 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_0_118)">
<rect x="0.5" width="14" height="14" rx="7" fill="#F79009"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.81723 4.55883C5.11013 4.26593 5.585 4.26593 5.87789 4.55883L7.55801 6.23895L9.23813 4.55883C9.53103 4.26593 10.0059 4.26593 10.2988 4.55883C10.5917 4.85172 10.5917 5.32659 10.2988 5.61949L8.61867 7.29961L10.2988 8.97973C10.5917 9.27262 10.5917 9.74749 10.2988 10.0404C10.0059 10.3333 9.53103 10.3333 9.23813 10.0404L7.55801 8.36027L5.87789 10.0404C5.585 10.3333 5.11013 10.3333 4.81723 10.0404C4.52434 9.74749 4.52434 9.27262 4.81723 8.97973L6.49735 7.29961L4.81723 5.61949C4.52434 5.32659 4.52434 4.85172 4.81723 4.55883Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_0_118">
<rect x="0.5" width="14" height="14" rx="7" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 908 B

View file

@ -13,9 +13,11 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
import { EnvironmentsDatatable } from '@/react/edge/edge-stacks/ItemView/EnvironmentsDatatable';
import { EdgeStackStatus } from '@/react/edge/edge-stacks/ListView/EdgeStackStatus';
export const componentsModule = angular
.module('portainer.edge.react.components', [])
.component('edgeStacksDatatableStatus', r2a(EdgeStackStatus, ['edgeStack']))
.component(
'edgeStackEnvironmentsDatatable',
r2a(withUIRouter(withReactQuery(EnvironmentsDatatable)), [])

View file

@ -1,6 +1,7 @@
import _ from 'lodash-es';
import './datatable.css';
import { ResourceControlOwnership as RCO } from '@/react/portainer/access-control/types';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
function isBetween(value, a, b) {
return (value >= a && value <= b) || (value >= b && value <= a);
@ -14,6 +15,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [
'PAGINATION_MAX_ITEMS',
function ($interval, PaginationService, DatatableService, PAGINATION_MAX_ITEMS) {
this.RCO = RCO;
this.isBE = isBE;
this.state = {
selectAll: false,

View file

@ -120,7 +120,7 @@ export const ngModule = angular
'fallbackImage',
r2a(FallbackImage, ['src', 'fallbackIcon', 'alt', 'size', 'className'])
)
.component('prIcon', r2a(Icon, ['className', 'icon', 'mode', 'size']))
.component('prIcon', r2a(Icon, ['className', 'icon', 'mode', 'size', 'spin']))
.component('reactQueryDevTools', r2a(ReactQueryDevtoolsWrapper, []))
.component(
'dashboardItem',

View file

@ -29,15 +29,15 @@ interface Props {
className?: string;
size?: IconSize;
mode?: IconMode;
spin?: boolean;
}
export function Icon({ icon, className, mode, size }: Props) {
const classes = clsx(
className,
'icon inline-flex',
{ [`icon-${mode}`]: mode },
{ [`icon-${size}`]: size }
);
export function Icon({ icon, className, mode, size, spin }: Props) {
const classes = clsx(className, 'icon inline-flex', {
[`icon-${mode}`]: mode,
[`icon-${size}`]: size,
'animate-spin-slow': spin,
});
if (typeof icon !== 'string') {
const Icon = isValidElementType(icon) ? icon : null;

View file

@ -6,6 +6,7 @@ import { EdgeStackStatus, StatusType } from '@/react/edge/edge-stacks/types';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { useParamState } from '@/react/hooks/useParamState';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Datatable } from '@@/datatables';
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
@ -20,17 +21,29 @@ export function EnvironmentsDatatable() {
const {
params: { stackId },
} = useCurrentStateAndParams();
const edgeStackQuery = useEdgeStack(stackId);
const edgeStackQuery = useEdgeStack(stackId, {
refetchInterval(data) {
if (!data) {
return 0;
}
return Object.values(data.Status).some((status) =>
status.Status.every((s) => s.Type === StatusType.Running)
)
? 0
: 10000;
},
});
const [page, setPage] = useState(0);
const [statusFilter, setStatusFilter] = useParamState<StatusType>(
'status',
parseStatusFilter
(value) => (value ? parseInt(value, 10) : undefined)
);
const tableState = useTableStateWithoutStorage('name');
const endpointsQuery = useEnvironmentList({
pageLimit: tableState.pageSize,
page,
page: page + 1,
search: tableState.search,
sort: tableState.sortBy.id as 'Group' | 'Name',
order: tableState.sortBy.desc ? 'desc' : 'asc',
@ -38,27 +51,32 @@ export function EnvironmentsDatatable() {
edgeStackStatus: statusFilter,
});
const currentFileVersion =
edgeStackQuery.data?.StackFileVersion?.toString() || '';
const gitConfigURL = edgeStackQuery.data?.GitConfig?.URL || '';
const gitConfigCommitHash = edgeStackQuery.data?.GitConfig?.ConfigHash || '';
const environments: Array<EdgeStackEnvironment> = useMemo(
() =>
endpointsQuery.environments.map((env) => ({
...env,
StackStatus:
edgeStackQuery.data?.Status[env.Id] ||
endpointsQuery.environments.map(
(env) =>
({
Details: {
Pending: true,
Acknowledged: false,
ImagesPulled: false,
Error: false,
Ok: false,
RemoteUpdateSuccess: false,
Remove: false,
},
EndpointID: env.Id,
Error: '',
} satisfies EdgeStackStatus),
})),
[edgeStackQuery.data?.Status, endpointsQuery.environments]
...env,
TargetFileVersion: currentFileVersion,
GitConfigURL: gitConfigURL,
TargetCommitHash: gitConfigCommitHash,
StackStatus: getEnvStackStatus(
env.Id,
edgeStackQuery.data?.Status[env.Id]
),
} satisfies EdgeStackEnvironment)
),
[
currentFileVersion,
edgeStackQuery.data?.Status,
endpointsQuery.environments,
gitConfigCommitHash,
gitConfigURL,
]
);
return (
@ -81,11 +99,11 @@ export function EnvironmentsDatatable() {
value={statusFilter}
onChange={(e) => setStatusFilter(e || undefined)}
options={[
{ value: 'Pending', label: 'Pending' },
{ value: 'Acknowledged', label: 'Acknowledged' },
{ value: 'ImagesPulled', label: 'Images pre-pulled' },
{ value: 'Ok', label: 'Deployed' },
{ value: 'Error', label: 'Failed' },
{ value: StatusType.Pending, label: 'Pending' },
{ value: StatusType.Acknowledged, label: 'Acknowledged' },
{ value: StatusType.ImagesPulled, label: 'Images pre-pulled' },
{ value: StatusType.Running, label: 'Deployed' },
{ value: StatusType.Error, label: 'Failed' },
]}
/>
</div>
@ -95,19 +113,31 @@ export function EnvironmentsDatatable() {
);
}
function parseStatusFilter(status: string | undefined): StatusType | undefined {
switch (status) {
case 'Pending':
return 'Pending';
case 'Acknowledged':
return 'Acknowledged';
case 'ImagesPulled':
return 'ImagesPulled';
case 'Ok':
return 'Ok';
case 'Error':
return 'Error';
default:
return undefined;
function getEnvStackStatus(
envId: EnvironmentId,
envStatus: EdgeStackStatus | undefined
) {
const pendingStatus = {
Type: StatusType.Pending,
Error: '',
Time: new Date().valueOf() / 1000,
};
let status = envStatus;
if (!status) {
status = {
EndpointID: envId,
DeploymentInfo: {
ConfigHash: '',
FileVersion: 0,
},
Status: [],
};
}
if (status.Status.length === 0) {
status.Status.push(pendingStatus);
}
return status;
}

View file

@ -2,13 +2,19 @@ import { CellContext, createColumnHelper } from '@tanstack/react-table';
import { ChevronDown, ChevronRight } from 'lucide-react';
import clsx from 'clsx';
import { useState } from 'react';
import _ from 'lodash';
import UpdatesAvailable from '@/assets/ico/icon_updates-available.svg?c';
import UpToDate from '@/assets/ico/icon_up-to-date.svg?c';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { getDashboardRoute } from '@/react/portainer/environments/utils';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
import { Link } from '@@/Link';
import { EdgeStackStatus } from '../../types';
import { DeploymentStatus, EdgeStackStatus, StatusType } from '../../types';
import { EnvironmentActions } from './EnvironmentActions';
import { ActionStatus } from './ActionStatus';
@ -16,20 +22,75 @@ import { EdgeStackEnvironment } from './types';
const columnHelper = createColumnHelper<EdgeStackEnvironment>();
export const columns = [
export const columns = _.compact([
columnHelper.accessor('Name', {
id: 'name',
header: 'Name',
cell({ row: { original: env } }) {
const { to, params } = getDashboardRoute(env);
return (
<Link to={to} params={params}>
{env.Name}
</Link>
);
},
}),
columnHelper.accessor((env) => endpointStatusLabel(env.StackStatus), {
columnHelper.accessor((env) => endpointStatusLabel(env.StackStatus.Status), {
id: 'status',
header: 'Status',
cell({ row: { original: env } }) {
return (
<ul className="list-none space-y-2">
{env.StackStatus.Status.map((s) => (
<li key={`status-${s.Type}-${s.Time}`}>
<Status value={s.Type} />
</li>
))}
</ul>
);
},
}),
columnHelper.accessor((env) => env.StackStatus.Error, {
id: 'error',
header: 'Error',
cell: ErrorCell,
columnHelper.accessor((env) => _.last(env.StackStatus.Status)?.Time, {
id: 'statusDate',
header: 'Time',
cell({ row: { original: env } }) {
return (
<ul className="list-none space-y-2">
{env.StackStatus.Status.map((s) => (
<li key={`time-${s.Type}-${s.Time}`}>
{isoDateFromTimestamp(s.Time)}
</li>
))}
</ul>
);
},
}),
...(isBE
? [
columnHelper.accessor((env) => endpointTargetVersionLabel(env), {
id: 'targetVersion',
header: 'Target version',
cell: TargetVersionCell,
}),
columnHelper.accessor(
(env) => endpointDeployedVersionLabel(env.StackStatus),
{
id: 'deployedVersion',
header: 'Deployed version',
cell: DeployedVersionCell,
}
),
]
: []),
columnHelper.accessor(
(env) =>
env.StackStatus.Status.find((s) => s.Type === StatusType.Error)?.Error,
{
id: 'error',
header: 'Error',
cell: ErrorCell,
}
),
...(isBE
? [
columnHelper.display({
@ -48,7 +109,7 @@ export const columns = [
}),
]
: []),
];
]);
function ErrorCell({ getValue }: CellContext<EdgeStackEnvironment, string>) {
const [isExpanded, setIsExpanded] = useState(false);
@ -77,30 +138,151 @@ function ErrorCell({ getValue }: CellContext<EdgeStackEnvironment, string>) {
);
}
function endpointStatusLabel(status: EdgeStackStatus) {
const details = (status && status.Details) || {};
function endpointStatusLabel(statusArray: Array<DeploymentStatus>) {
const labels = [];
if (details.Acknowledged) {
labels.push('Acknowledged');
}
if (details.ImagesPulled) {
labels.push('Images pre-pulled');
}
if (details.Ok) {
labels.push('Deployed');
}
if (details.Error) {
labels.push('Failed');
}
statusArray.forEach((status) => {
if (status.Type === StatusType.Acknowledged) {
labels.push('Acknowledged');
}
if (status.Type === StatusType.ImagesPulled) {
labels.push('Images pre-pulled');
}
if (status.Type === StatusType.Running) {
labels.push('Deployed');
}
if (status.Type === StatusType.Error) {
labels.push('Failed');
}
});
if (!labels.length) {
labels.push('Pending');
}
return labels.join(', ');
return _.uniq(labels).join(', ');
}
function TargetVersionCell({
row,
getValue,
}: CellContext<EdgeStackEnvironment, string>) {
const value = getValue();
if (!value) {
return '';
}
return (
<>
{row.original.TargetCommitHash ? (
<div>
<a
href={`${row.original.GitConfigURL}/commit/${row.original.TargetCommitHash}`}
target="_blank"
rel="noreferrer"
>
{value}
</a>
</div>
) : (
<div>{value}</div>
)}
</>
);
}
function endpointTargetVersionLabel(env: EdgeStackEnvironment) {
if (env.TargetCommitHash) {
return env.TargetCommitHash.slice(0, 7).toString();
}
return env.TargetFileVersion.toString() || '';
}
function DeployedVersionCell({
row,
getValue,
}: CellContext<EdgeStackEnvironment, string>) {
const value = getValue();
if (!value || value === '0') {
return (
<div>
<Icon icon={UpdatesAvailable} className="!mr-2" />
</div>
);
}
let statusIcon = <Icon icon={UpToDate} className="!mr-2" />;
if (
(row.original.TargetCommitHash &&
row.original.TargetCommitHash.slice(0, 7) !== value) ||
(!row.original.TargetCommitHash && row.original.TargetFileVersion !== value)
) {
statusIcon = <Icon icon={UpdatesAvailable} className="!mr-2" />;
}
return (
<>
{row.original.TargetCommitHash ? (
<div>
{statusIcon}
<a
href={`${row.original.GitConfigURL}/commit/${row.original.TargetCommitHash}`}
target="_blank"
rel="noreferrer"
>
{value}
</a>
</div>
) : (
<div>
{statusIcon}
{value}
</div>
)}
</>
);
}
function endpointDeployedVersionLabel(status: EdgeStackStatus) {
if (status.DeploymentInfo?.ConfigHash) {
return status.DeploymentInfo?.ConfigHash.slice(0, 7).toString();
}
return status.DeploymentInfo?.FileVersion.toString() || '';
}
function Status({ value }: { value: StatusType }) {
const color = getStateColor(value);
return (
<div className="flex items-center gap-2">
<span
className={clsx('h-2 w-2 rounded-full', {
'bg-orange-5': color === 'orange',
'bg-green-5': color === 'green',
'bg-error-5': color === 'red',
})}
/>
<span>{_.startCase(StatusType[value])}</span>
</div>
);
}
function getStateColor(type: StatusType): 'orange' | 'green' | 'red' {
switch (type) {
case StatusType.Acknowledged:
case StatusType.ImagesPulled:
case StatusType.DeploymentReceived:
case StatusType.Running:
case StatusType.RemoteUpdateSuccess:
case StatusType.Removed:
return 'green';
case StatusType.Error:
return 'red';
case StatusType.Pending:
case StatusType.Deploying:
case StatusType.Removing:
default:
return 'orange';
}
}

View file

@ -4,4 +4,7 @@ import { EdgeStackStatus } from '../../types';
export type EdgeStackEnvironment = Environment & {
StackStatus: EdgeStackStatus;
TargetFileVersion: string;
GitConfigURL: string;
TargetCommitHash: string;
};

View file

@ -0,0 +1,87 @@
import _ from 'lodash';
import {
AlertTriangle,
CheckCircle,
type Icon as IconType,
Loader2,
XCircle,
} from 'lucide-react';
import { Icon, IconMode } from '@@/Icon';
import { DeploymentStatus, EdgeStack, StatusType } from '../types';
export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
const status = Object.values(edgeStack.Status);
const lastStatus = _.compact(status.map((s) => _.last(s.Status)));
const { icon, label, mode, spin } = getStatus(
edgeStack.NumDeployments,
lastStatus
);
return (
<div className="mx-auto inline-flex items-center gap-2">
{icon && <Icon icon={icon} spin={spin} mode={mode} />}
{label}
</div>
);
}
function getStatus(
numDeployments: number,
envStatus: Array<DeploymentStatus>
): {
label: string;
icon?: IconType;
spin?: boolean;
mode?: IconMode;
} {
if (envStatus.length < numDeployments) {
return {
label: 'Deploying',
icon: Loader2,
spin: true,
mode: 'primary',
};
}
const allFailed = envStatus.every((s) => s.Type === StatusType.Error);
if (allFailed) {
return {
label: 'Failed',
icon: XCircle,
mode: 'danger',
};
}
const allRunning = envStatus.every((s) => s.Type === StatusType.Running);
if (allRunning) {
return {
label: 'Running',
icon: CheckCircle,
mode: 'success',
};
}
const hasDeploying = envStatus.some((s) => s.Type === StatusType.Deploying);
const hasRunning = envStatus.some((s) => s.Type === StatusType.Running);
const hasFailed = envStatus.some((s) => s.Type === StatusType.Error);
if (hasRunning && hasFailed && !hasDeploying) {
return {
label: 'Partially Running',
icon: AlertTriangle,
mode: 'warning',
};
}
return {
label: 'Deploying',
icon: Loader2,
spin: true,
mode: 'primary',
};
}

View file

@ -38,10 +38,10 @@ export function DeploymentCounter({
return (
<span
className={clsx(styles.root, {
[styles.statusOk]: type === 'Ok',
[styles.statusError]: type === 'Error',
[styles.statusAcknowledged]: type === 'Acknowledged',
[styles.statusImagesPulled]: type === 'ImagesPulled',
[styles.statusOk]: type === StatusType.Running,
[styles.statusError]: type === StatusType.Error,
[styles.statusAcknowledged]: type === StatusType.Acknowledged,
[styles.statusImagesPulled]: type === StatusType.ImagesPulled,
[styles.statusTotal]: type === undefined,
})}
>

View file

@ -4,7 +4,7 @@ import { Datatable } from '@@/datatables';
import { useTableState } from '@@/datatables/useTableState';
import { useEdgeStacks } from '../../queries/useEdgeStacks';
import { EdgeStack } from '../../types';
import { EdgeStack, StatusType } from '../../types';
import { createStore } from './store';
import { columns } from './columns';
@ -51,11 +51,16 @@ export function EdgeStacksDatatable() {
function aggregateStackStatus(stackStatus: EdgeStack['Status']) {
const aggregateStatus = { ok: 0, error: 0, acknowledged: 0, imagesPulled: 0 };
return Object.values(stackStatus).reduce((acc, envStatus) => {
acc.ok += Number(envStatus.Details.Ok);
acc.error += Number(envStatus.Details.Error);
acc.acknowledged += Number(envStatus.Details.Acknowledged);
acc.imagesPulled += Number(envStatus.Details.ImagesPulled);
return acc;
}, aggregateStatus);
return Object.values(stackStatus).reduce(
(acc, envStatus) =>
envStatus.Status.reduce((acc, status) => {
const { Type } = status;
acc.ok += Number(Type === StatusType.Running);
acc.error += Number(Type === StatusType.Error);
acc.acknowledged += Number(Type === StatusType.Acknowledged);
acc.imagesPulled += Number(Type === StatusType.ImagesPulled);
return acc;
}, acc),
aggregateStatus
);
}

View file

@ -0,0 +1,87 @@
import _ from 'lodash';
import {
AlertTriangle,
CheckCircle,
type Icon as IconType,
Loader2,
XCircle,
} from 'lucide-react';
import { Icon, IconMode } from '@@/Icon';
import { DeploymentStatus, EdgeStack, StatusType } from '../../types';
export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
const status = Object.values(edgeStack.Status);
const lastStatus = _.compact(status.map((s) => _.last(s.Status)));
const { icon, label, mode, spin } = getStatus(
edgeStack.NumDeployments,
lastStatus
);
return (
<div className="mx-auto inline-flex items-center gap-2">
{icon && <Icon icon={icon} spin={spin} mode={mode} />}
{label}
</div>
);
}
function getStatus(
numDeployments: number,
envStatus: Array<DeploymentStatus>
): {
label: string;
icon?: IconType;
spin?: boolean;
mode?: IconMode;
} {
if (envStatus.length < numDeployments) {
return {
label: 'Deploying',
icon: Loader2,
spin: true,
mode: 'primary',
};
}
const allFailed = envStatus.every((s) => s.Type === StatusType.Error);
if (allFailed) {
return {
label: 'Failed',
icon: XCircle,
mode: 'danger',
};
}
const allRunning = envStatus.every((s) => s.Type === StatusType.Running);
if (allRunning) {
return {
label: 'Running',
icon: CheckCircle,
mode: 'success',
};
}
const hasDeploying = envStatus.some((s) => s.Type === StatusType.Deploying);
const hasRunning = envStatus.some((s) => s.Type === StatusType.Running);
const hasFailed = envStatus.some((s) => s.Type === StatusType.Error);
if (hasRunning && hasFailed && !hasDeploying) {
return {
label: 'Partially Running',
icon: AlertTriangle,
mode: 'warning',
};
}
return {
label: 'Deploying',
icon: Loader2,
spin: true,
mode: 'primary',
};
}

View file

@ -6,6 +6,9 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { buildNameColumn } from '@@/datatables/NameCell';
import { StatusType } from '../../types';
import { EdgeStackStatus } from '../EdgeStackStatus';
import { DecoratedEdgeStack } from './types';
import { DeploymentCounter, DeploymentCounterLink } from './DeploymentCounter';
@ -25,7 +28,7 @@ export const columns = _.compact([
cell: ({ getValue, row }) => (
<DeploymentCounterLink
count={getValue()}
type="Acknowledged"
type={StatusType.Acknowledged}
stackId={row.original.Id}
/>
),
@ -39,7 +42,7 @@ export const columns = _.compact([
cell: ({ getValue, row }) => (
<DeploymentCounterLink
count={getValue()}
type="ImagesPulled"
type={StatusType.ImagesPulled}
stackId={row.original.Id}
/>
),
@ -54,7 +57,7 @@ export const columns = _.compact([
cell: ({ getValue, row }) => (
<DeploymentCounterLink
count={getValue()}
type="Ok"
type={StatusType.Running}
stackId={row.original.Id}
/>
),
@ -69,7 +72,7 @@ export const columns = _.compact([
cell: ({ getValue, row }) => (
<DeploymentCounterLink
count={getValue()}
type="Error"
type={StatusType.Error}
stackId={row.original.Id}
/>
),
@ -79,6 +82,19 @@ export const columns = _.compact([
className: '[&>*]:justify-center',
},
}),
columnHelper.accessor('Status', {
header: 'Status',
cell: ({ row }) => (
<div className="w-full text-center">
<EdgeStackStatus edgeStack={row.original} />
</div>
),
enableSorting: false,
enableHiding: false,
meta: {
className: '[&>*]:justify-center',
},
}),
columnHelper.accessor('NumDeployments', {
header: 'Deployments',
cell: ({ getValue }) => (

View file

@ -8,10 +8,24 @@ import { EdgeStack } from '../types';
import { buildUrl } from './buildUrl';
import { queryKeys } from './query-keys';
export function useEdgeStack(id?: EdgeStack['Id']) {
export function useEdgeStack(
id?: EdgeStack['Id'],
{
refetchInterval,
}: {
/**
* If set to a number, the query will continuously refetch at this frequency in milliseconds. If set to a function, the function will be executed with the latest data and query to compute a frequency Defaults to false.
*/
refetchInterval?:
| number
| false
| ((data?: Awaited<ReturnType<typeof getEdgeStack>>) => false | number);
} = {}
) {
return useQuery(id ? queryKeys.item(id) : [], () => getEdgeStack(id), {
...withError('Failed loading Edge stack'),
enabled: !!id,
refetchInterval,
});
}

View file

@ -6,6 +6,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EdgeStack } from '../types';
import { buildUrl } from './buildUrl';
import { queryKeys } from './query-keys';
export function useEdgeStacks<T = Array<EdgeStack>>({
select,
@ -19,7 +20,7 @@ export function useEdgeStacks<T = Array<EdgeStack>>({
select?: (stacks: EdgeStack[]) => T;
refetchInterval?: number | false | ((data?: T) => false | number);
} = {}) {
return useQuery(['edge_stacks'], () => getEdgeStacks(), {
return useQuery(queryKeys.base(), () => getEdgeStacks(), {
...withError('Failed loading Edge stack'),
select,
refetchInterval,

View file

@ -9,22 +9,44 @@ import { EnvVar } from '@@/form-components/EnvironmentVariablesFieldset/types';
import { EdgeGroup } from '../edge-groups/types';
interface EdgeStackStatusDetails {
Pending: boolean;
Ok: boolean;
Error: boolean;
Acknowledged: boolean;
Remove: boolean;
RemoteUpdateSuccess: boolean;
ImagesPulled: boolean;
export enum StatusType {
/** Pending represents a pending edge stack */
Pending,
/** DeploymentReceived represents an edge environment which received the edge stack deployment */
DeploymentReceived,
/** Error represents an edge environment which failed to deploy its edge stack */
Error,
/** Acknowledged represents an acknowledged edge stack */
Acknowledged,
/** Removed represents a removed edge stack */
Removed,
/** StatusRemoteUpdateSuccess represents a successfully updated edge stack */
RemoteUpdateSuccess,
/** ImagesPulled represents a successfully images-pulling */
ImagesPulled,
/** Running represents a running Edge stack */
Running,
/** Deploying represents an Edge stack which is being deployed */
Deploying,
/** Removing represents an Edge stack which is being removed */
Removing,
}
export type StatusType = keyof EdgeStackStatusDetails;
export interface DeploymentStatus {
Type: StatusType;
Error: string;
Time: number;
}
interface EdgeStackDeploymentInfo {
FileVersion: number;
ConfigHash: string;
}
export interface EdgeStackStatus {
Details: EdgeStackStatusDetails;
Error: string;
Status: Array<DeploymentStatus>;
EndpointID: EnvironmentId;
DeploymentInfo?: EdgeStackDeploymentInfo;
}
export enum DeploymentType {

View file

@ -0,0 +1,16 @@
import { DeploymentStatus } from '../types';
/**
* returns the latest status object of each type
*/
export function uniqueStatus(statusArray: Array<DeploymentStatus> = []) {
// keep only the last status object of each type, assume that the last status is the most recent
return statusArray.reduce((acc, status) => {
const index = acc.findIndex((s) => s.Type === status.Type);
if (index === -1) {
return [...acc, status];
}
return [...acc.slice(0, index), ...acc.slice(index + 1), status];
}, [] as Array<DeploymentStatus>);
}