1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +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

@ -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;
};