mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(edge/stacks): increase status transparency [EE-5554] (#9094)
This commit is contained in:
parent
db61fb149b
commit
0bcb57568c
45 changed files with 1305 additions and 316 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,7 @@ import { EdgeStackStatus } from '../../types';
|
|||
|
||||
export type EdgeStackEnvironment = Environment & {
|
||||
StackStatus: EdgeStackStatus;
|
||||
TargetFileVersion: string;
|
||||
GitConfigURL: string;
|
||||
TargetCommitHash: string;
|
||||
};
|
||||
|
|
87
app/react/edge/edge-stacks/ListView/EdgeStackStatus.tsx
Normal file
87
app/react/edge/edge-stacks/ListView/EdgeStackStatus.tsx
Normal 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',
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
|
@ -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 }) => (
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
16
app/react/edge/edge-stacks/utils/uniqueStatus.ts
Normal file
16
app/react/edge/edge-stacks/utils/uniqueStatus.ts
Normal 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>);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue