mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(app/edge-stacks): summarize the edge stack statuses in the backend (#818)
This commit is contained in:
parent
363a62d885
commit
e1c480d3c3
21 changed files with 645 additions and 312 deletions
|
@ -5,7 +5,6 @@ import { useTableState } from '@@/datatables/useTableState';
|
|||
import { getColumnVisibilityState } from '@@/datatables/ColumnVisibilityMenu';
|
||||
|
||||
import { useEdgeStacks } from '../../queries/useEdgeStacks';
|
||||
import { EdgeStack, StatusType } from '../../types';
|
||||
|
||||
import { createStore } from './store';
|
||||
import { columns } from './columns';
|
||||
|
@ -20,11 +19,7 @@ const settingsStore = createStore(tableKey);
|
|||
export function EdgeStacksDatatable() {
|
||||
const tableState = useTableState(settingsStore, tableKey);
|
||||
const edgeStacksQuery = useEdgeStacks<Array<DecoratedEdgeStack>>({
|
||||
select: (edgeStacks) =>
|
||||
edgeStacks.map((edgeStack) => ({
|
||||
...edgeStack,
|
||||
aggregatedStatus: aggregateStackStatus(edgeStack.Status),
|
||||
})),
|
||||
params: { summarizeStatuses: true },
|
||||
refetchInterval: tableState.autoRefreshRate * 1000,
|
||||
});
|
||||
|
||||
|
@ -50,16 +45,3 @@ export function EdgeStacksDatatable() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function aggregateStackStatus(stackStatus: EdgeStack['Status']) {
|
||||
const aggregateStatus: Partial<Record<StatusType, number>> = {};
|
||||
return Object.values(stackStatus).reduce(
|
||||
(acc, envStatus) =>
|
||||
envStatus.Status.reduce((acc, status) => {
|
||||
const { Type } = status;
|
||||
acc[Type] = (acc[Type] || 0) + 1;
|
||||
return acc;
|
||||
}, acc),
|
||||
aggregateStatus
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import _ from 'lodash';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
|
@ -6,35 +5,22 @@ import {
|
|||
Loader2,
|
||||
XCircle,
|
||||
MinusCircle,
|
||||
PauseCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||
import { isVersionSmaller } from '@/react/common/semver-utils';
|
||||
|
||||
import { Icon, IconMode } from '@@/Icon';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
import { DeploymentStatus, EdgeStack, StatusType } from '../../types';
|
||||
import { DecoratedEdgeStack, StatusSummary, SummarizedStatus } from './types';
|
||||
|
||||
export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
|
||||
const status = Object.values(edgeStack.Status);
|
||||
const lastStatus = _.compact(status.map((s) => _.last(s.Status)));
|
||||
export function EdgeStackStatus({
|
||||
edgeStack,
|
||||
}: {
|
||||
edgeStack: DecoratedEdgeStack;
|
||||
}) {
|
||||
const { StatusSummary } = edgeStack;
|
||||
|
||||
const environmentsQuery = useEnvironmentList({ edgeStackId: edgeStack.Id });
|
||||
|
||||
if (environmentsQuery.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasOldVersion = environmentsQuery.environments.some((env) =>
|
||||
isVersionSmaller(env.Agent.Version, '2.19.0')
|
||||
);
|
||||
|
||||
const { icon, label, mode, spin, tooltip } = getStatus(
|
||||
edgeStack.NumDeployments,
|
||||
lastStatus,
|
||||
hasOldVersion
|
||||
);
|
||||
const { icon, label, mode, spin, tooltip } = getStatus(StatusSummary);
|
||||
|
||||
return (
|
||||
<div className="mx-auto inline-flex items-center gap-2">
|
||||
|
@ -45,106 +31,68 @@ export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
|
|||
);
|
||||
}
|
||||
|
||||
function getStatus(
|
||||
numDeployments: number,
|
||||
envStatus: Array<DeploymentStatus>,
|
||||
hasOldVersion: boolean
|
||||
): {
|
||||
function getStatus(summary?: StatusSummary): {
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
spin?: boolean;
|
||||
mode?: IconMode;
|
||||
tooltip?: string;
|
||||
} {
|
||||
if (!numDeployments || hasOldVersion) {
|
||||
if (!summary) {
|
||||
return {
|
||||
label: 'Unavailable',
|
||||
icon: MinusCircle,
|
||||
mode: 'secondary',
|
||||
tooltip: getUnavailableTooltip(),
|
||||
tooltip: 'Status summary is unavailable',
|
||||
};
|
||||
}
|
||||
const { Status, Reason } = summary;
|
||||
|
||||
if (!envStatus.length) {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
if (envStatus.length < numDeployments) {
|
||||
return {
|
||||
label: 'Deploying',
|
||||
icon: Loader2,
|
||||
spin: true,
|
||||
mode: 'primary',
|
||||
};
|
||||
}
|
||||
|
||||
const allCompleted = envStatus.every((s) => s.Type === StatusType.Completed);
|
||||
|
||||
if (allCompleted) {
|
||||
return {
|
||||
label: 'Completed',
|
||||
icon: CheckCircle,
|
||||
mode: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
const allRunning = envStatus.every(
|
||||
(s) =>
|
||||
s.Type === StatusType.Running ||
|
||||
(s.Type === StatusType.DeploymentReceived && hasOldVersion)
|
||||
);
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
function getUnavailableTooltip() {
|
||||
if (!numDeployments) {
|
||||
return 'Your edge stack is currently unavailable due to the absence of an available environment in your edge group';
|
||||
}
|
||||
|
||||
if (hasOldVersion) {
|
||||
return 'Please note that the new status feature for the Edge stack is only available for Edge Agent versions 2.19.0 and above. To access the status of your edge stack, it is essential to upgrade your Edge Agent to a corresponding version that is compatible with your Portainer server.';
|
||||
}
|
||||
|
||||
return '';
|
||||
switch (Status) {
|
||||
case SummarizedStatus.Deploying:
|
||||
return {
|
||||
label: 'Deploying',
|
||||
icon: Loader2,
|
||||
spin: true,
|
||||
mode: 'primary',
|
||||
};
|
||||
case SummarizedStatus.Failed:
|
||||
return {
|
||||
label: 'Failed',
|
||||
icon: XCircle,
|
||||
mode: 'danger',
|
||||
};
|
||||
case SummarizedStatus.Paused:
|
||||
return {
|
||||
label: 'Paused',
|
||||
icon: PauseCircle,
|
||||
mode: 'warning',
|
||||
};
|
||||
case SummarizedStatus.PartiallyRunning:
|
||||
return {
|
||||
label: 'Partially Running',
|
||||
icon: AlertTriangle,
|
||||
mode: 'warning',
|
||||
};
|
||||
case SummarizedStatus.Completed:
|
||||
return {
|
||||
label: 'Completed',
|
||||
icon: CheckCircle,
|
||||
mode: 'success',
|
||||
};
|
||||
case SummarizedStatus.Running:
|
||||
return {
|
||||
label: 'Running',
|
||||
icon: CheckCircle,
|
||||
mode: 'success',
|
||||
};
|
||||
case SummarizedStatus.Unavailable:
|
||||
default:
|
||||
return {
|
||||
label: 'Unavailable',
|
||||
icon: MinusCircle,
|
||||
mode: 'secondary',
|
||||
tooltip: Reason,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@ import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
|||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { GitCommitLink } from '@/react/portainer/gitops/GitCommitLink';
|
||||
|
||||
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||
import { buildNameColumnFromObject } from '@@/datatables/buildNameColumn';
|
||||
import { Link } from '@@/Link';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
import { StatusType } from '../../types';
|
||||
|
||||
|
@ -17,14 +18,15 @@ import { DeploymentCounter } from './DeploymentCounter';
|
|||
const columnHelper = createColumnHelper<DecoratedEdgeStack>();
|
||||
|
||||
export const columns = _.compact([
|
||||
buildNameColumn<DecoratedEdgeStack>(
|
||||
'Name',
|
||||
'edge.stacks.edit',
|
||||
'edge-stacks-name',
|
||||
'stackId'
|
||||
),
|
||||
buildNameColumnFromObject<DecoratedEdgeStack>({
|
||||
nameKey: 'Name',
|
||||
path: 'edge.stacks.edit',
|
||||
dataCy: 'edge-stacks-name',
|
||||
idParam: 'stackId',
|
||||
}),
|
||||
columnHelper.accessor(
|
||||
(item) => item.aggregatedStatus[StatusType.Acknowledged] || 0,
|
||||
(item) =>
|
||||
item.StatusSummary?.AggregatedStatus?.[StatusType.Acknowledged] || 0,
|
||||
{
|
||||
header: 'Acknowledged',
|
||||
enableSorting: false,
|
||||
|
@ -43,7 +45,8 @@ export const columns = _.compact([
|
|||
),
|
||||
isBE &&
|
||||
columnHelper.accessor(
|
||||
(item) => item.aggregatedStatus[StatusType.ImagesPulled] || 0,
|
||||
(item) =>
|
||||
item.StatusSummary?.AggregatedStatus?.[StatusType.ImagesPulled] || 0,
|
||||
{
|
||||
header: 'Images pre-pulled',
|
||||
cell: ({ getValue, row: { original: item } }) => {
|
||||
|
@ -67,7 +70,9 @@ export const columns = _.compact([
|
|||
}
|
||||
),
|
||||
columnHelper.accessor(
|
||||
(item) => item.aggregatedStatus[StatusType.DeploymentReceived] || 0,
|
||||
(item) =>
|
||||
item.StatusSummary?.AggregatedStatus?.[StatusType.DeploymentReceived] ||
|
||||
0,
|
||||
{
|
||||
header: 'Deployments received',
|
||||
cell: ({ getValue, row }) => (
|
||||
|
@ -85,7 +90,7 @@ export const columns = _.compact([
|
|||
}
|
||||
),
|
||||
columnHelper.accessor(
|
||||
(item) => item.aggregatedStatus[StatusType.Error] || 0,
|
||||
(item) => item.StatusSummary?.AggregatedStatus?.[StatusType.Error] || 0,
|
||||
{
|
||||
header: 'Deployments failed',
|
||||
cell: ({ getValue, row }) => {
|
||||
|
@ -123,7 +128,7 @@ export const columns = _.compact([
|
|||
}
|
||||
),
|
||||
columnHelper.accessor('Status', {
|
||||
header: 'Status',
|
||||
header: StatusHeader,
|
||||
cell: ({ row }) => (
|
||||
<div className="w-full text-center">
|
||||
<EdgeStackStatus edgeStack={row.original} />
|
||||
|
@ -167,3 +172,27 @@ export const columns = _.compact([
|
|||
}
|
||||
),
|
||||
]);
|
||||
|
||||
function StatusHeader() {
|
||||
return (
|
||||
<>
|
||||
Status
|
||||
<Tooltip
|
||||
position="top"
|
||||
message={
|
||||
<>
|
||||
<div>
|
||||
The status feature for the Edge stack is only available for Edge
|
||||
Agent versions 2.19.0 and above.
|
||||
</div>
|
||||
<div>
|
||||
To access the status of your edge stack, it is essential to
|
||||
upgrade your Edge Agent to a corresponding version that is
|
||||
compatible with your Portainer server.
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
import { EdgeStack, StatusType } from '../../types';
|
||||
|
||||
export type DecoratedEdgeStack = EdgeStack & {
|
||||
aggregatedStatus: Partial<Record<StatusType, number>>;
|
||||
export enum SummarizedStatus {
|
||||
Unavailable = 'Unavailable',
|
||||
Deploying = 'Deploying',
|
||||
Failed = 'Failed',
|
||||
Paused = 'Paused',
|
||||
PartiallyRunning = 'PartiallyRunning',
|
||||
Completed = 'Completed',
|
||||
Running = 'Running',
|
||||
}
|
||||
|
||||
export type StatusSummary = {
|
||||
AggregatedStatus?: Partial<Record<StatusType, number>>;
|
||||
Status: SummarizedStatus;
|
||||
Reason: string;
|
||||
};
|
||||
|
||||
export type DecoratedEdgeStack = EdgeStack & {
|
||||
StatusSummary?: StatusSummary;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { EdgeStack } from '../types';
|
||||
|
@ -8,28 +8,30 @@ import { EdgeStack } from '../types';
|
|||
import { buildUrl } from './buildUrl';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useEdgeStacks<T = Array<EdgeStack>>({
|
||||
select,
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
type QueryParams = {
|
||||
summarizeStatuses?: boolean;
|
||||
};
|
||||
|
||||
export function useEdgeStacks<T extends EdgeStack[] = EdgeStack[]>({
|
||||
params,
|
||||
refetchInterval,
|
||||
}: {
|
||||
select?: (stacks: EdgeStack[]) => T;
|
||||
params?: QueryParams;
|
||||
refetchInterval?: number | false | ((data?: T) => false | number);
|
||||
} = {}) {
|
||||
return useQuery(queryKeys.base(), () => getEdgeStacks(), {
|
||||
...withError('Failed loading Edge stack'),
|
||||
select,
|
||||
return useQuery({
|
||||
queryKey: queryKeys.base(),
|
||||
queryFn: () => getEdgeStacks<T>(params),
|
||||
refetchInterval,
|
||||
...withGlobalError('Failed loading Edge stack'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getEdgeStacks() {
|
||||
async function getEdgeStacks<T extends EdgeStack[] = EdgeStack[]>(
|
||||
params: QueryParams = {}
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<EdgeStack[]>(buildUrl());
|
||||
const { data } = await axios.get<T>(buildUrl(), { params });
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue