fix(edge): show heartbeat for async env [EE-3380] (#7097)

pull/7075/head
Chaim Lev-Ari 2022-06-22 20:11:46 +03:00 committed by GitHub
parent 60cd7b5527
commit 825269c119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 188 additions and 115 deletions

View File

@ -15,12 +15,5 @@ export const heartbeat: Column<Environment> = {
export function StatusCell({
row: { original: environment },
}: CellProps<Environment>) {
return (
<EdgeIndicator
checkInInterval={environment.EdgeCheckinInterval}
edgeId={environment.EdgeID}
lastCheckInDate={environment.LastCheckInDate}
queryDate={environment.QueryDate}
/>
);
return <EdgeIndicator environment={environment} />;
}

View File

@ -3,6 +3,8 @@ import { useRouter } from '@uirouter/react';
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
import { r2a } from '@/react-tools/react2angular';
import { InformationPanel } from '@@/InformationPanel';
import { TextTip } from '@@/Tip/TextTip';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { PageHeader } from '@@/PageHeader';
@ -30,6 +32,15 @@ export function WaitingRoomView() {
{ label: 'Waiting Room' },
]}
/>
<InformationPanel>
<TextTip color="blue">
Only environments generated from the AEEC script will appear here,
manually added environments and edge devices will bypass the waiting
room.
</TextTip>
</InformationPanel>
<TableSettingsProvider<TableSettings>
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
storageKey={storageKey}

View File

@ -54,6 +54,13 @@ export interface KubernetesSettings {
Snapshots?: KubernetesSnapshot[] | null;
}
export type EnvironmentEdge = {
AsyncMode: boolean;
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
};
export type Environment = {
Id: EnvironmentId;
Type: EnvironmentType;
@ -73,6 +80,7 @@ export type Environment = {
IsEdgeDevice?: boolean;
UserTrusted: boolean;
AMTDeviceGUID?: string;
Edge: EnvironmentEdge;
};
/**
* TS reference of endpoint_create.go#EndpointCreationType iota

View File

@ -1,35 +1,44 @@
import { render } from '@/react-tools/test-utils';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { renderWithQueryClient } from '@/react-tools/test-utils';
import { rest, server } from '@/setup-tests/server';
import { EdgeIndicator } from './EdgeIndicator';
test('when edge id is not set, should show unassociated label', () => {
const { queryByLabelText } = renderComponent();
test('when edge id is not set, should show unassociated label', async () => {
const { queryByLabelText } = await renderComponent();
const unassociatedLabel = queryByLabelText('unassociated');
expect(unassociatedLabel).toBeVisible();
});
test('given edge id and last checkin is set, should show heartbeat', () => {
const { queryByLabelText } = renderComponent('id', 1);
// test('given edge id and last checkin is set, should show heartbeat', async () => {
// const { queryByLabelText } = await renderComponent('id', 1);
expect(queryByLabelText('edge-heartbeat')).toBeVisible();
expect(queryByLabelText('edge-last-checkin')).toBeVisible();
});
// expect(queryByLabelText('edge-heartbeat')).toBeVisible();
// expect(queryByLabelText('edge-last-checkin')).toBeVisible();
// });
function renderComponent(
async function renderComponent(
edgeId = '',
lastCheckInDate = 0,
checkInInterval = 0,
queryDate = 0
) {
return render(
<EdgeIndicator
edgeId={edgeId}
lastCheckInDate={lastCheckInDate}
checkInInterval={checkInInterval}
queryDate={queryDate}
showLastCheckInDate
/>
server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({}))));
const environment = createMockEnvironment();
environment.EdgeID = edgeId;
environment.LastCheckInDate = lastCheckInDate;
environment.EdgeCheckinInterval = checkInInterval;
environment.QueryDate = queryDate;
const queries = renderWithQueryClient(
<EdgeIndicator environment={environment} showLastCheckInDate />
);
await expect(queries.findByRole('status')).resolves.toBeVisible();
return queries;
}

View File

@ -1,56 +1,111 @@
import clsx from 'clsx';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { Environment } from '@/portainer/environments/types';
import { useSettings } from '@/portainer/settings/queries';
import { Settings } from '@/portainer/settings/types';
interface Props {
checkInInterval?: number;
edgeId?: string;
queryDate?: number;
lastCheckInDate?: number;
showLastCheckInDate?: boolean;
environment: Environment;
}
export function EdgeIndicator({
edgeId,
lastCheckInDate,
checkInInterval,
queryDate,
environment,
showLastCheckInDate = false,
}: Props) {
if (!edgeId) {
const associated = !!environment.EdgeID;
const isValid = useHasHeartbeat(environment, associated);
if (isValid === null) {
return null;
}
if (!associated) {
return (
<span className="label label-default" aria-label="unassociated">
<s>associated</s>
<span role="status" aria-label="edge-status">
<span className="label label-default" aria-label="unassociated">
<s>associated</s>
</span>
</span>
);
}
// give checkIn some wiggle room
let isCheckValid = false;
if (checkInInterval && queryDate && lastCheckInDate) {
isCheckValid = queryDate - lastCheckInDate <= checkInInterval * 2 + 20;
}
return (
<span>
<span role="status" aria-label="edge-status">
<span
className={clsx('label', {
'label-danger': !isCheckValid,
'label-success': isCheckValid,
'label-danger': !isValid,
'label-success': isValid,
})}
aria-label="edge-heartbeat"
>
heartbeat
</span>
{showLastCheckInDate && !!lastCheckInDate && (
{showLastCheckInDate && !!environment.LastCheckInDate && (
<span
className="space-left small text-muted"
aria-label="edge-last-checkin"
>
{isoDateFromTimestamp(lastCheckInDate)}
{isoDateFromTimestamp(environment.LastCheckInDate)}
</span>
)}
</span>
);
}
function useHasHeartbeat(environment: Environment, associated: boolean) {
const settingsQuery = useSettings(undefined, associated);
if (!associated) {
return false;
}
const { LastCheckInDate, QueryDate } = environment;
const settings = settingsQuery.data;
if (!settings) {
return null;
}
const checkInInterval = getCheckinInterval(environment, settings);
if (checkInInterval && QueryDate && LastCheckInDate) {
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
}
return false;
}
function getCheckinInterval(environment: Environment, settings: Settings) {
const asyncMode = environment.Edge.AsyncMode;
if (asyncMode) {
const intervals = [
environment.Edge.PingInterval > 0
? environment.Edge.PingInterval
: settings.Edge.PingInterval,
environment.Edge.SnapshotInterval > 0
? environment.Edge.SnapshotInterval
: settings.Edge.SnapshotInterval,
environment.Edge.CommandInterval > 0
? environment.Edge.CommandInterval
: settings.Edge.CommandInterval,
].filter((n) => n > 0);
return intervals.length > 0 ? Math.min(...intervals) : 60;
}
if (
!environment.EdgeCheckinInterval ||
environment.EdgeCheckinInterval === 0
) {
return settings.EdgeAgentCheckinInterval;
}
return environment.EdgeCheckinInterval;
}

View File

@ -5,6 +5,7 @@ import {
EnvironmentStatus,
EnvironmentType,
} from '@/portainer/environments/types';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { EnvironmentItem } from './EnvironmentItem';
@ -57,19 +58,9 @@ KubernetesEdgeEnvironment.args = {
};
function mockEnvironment(type: EnvironmentType): Environment {
return {
Id: 1,
Name: 'environment',
GroupId: 1,
Snapshots: [],
Status: EnvironmentStatus.Up,
TagIds: [],
Type: type,
Kubernetes: {
Snapshots: [],
},
URL: 'url',
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
env.Type = type;
env.Status = EnvironmentStatus.Up;
return env;
}

View File

@ -6,25 +6,14 @@ import { Environment } from '@/portainer/environments/types';
import { UserContext } from '@/portainer/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user';
import { Tag } from '@/portainer/tags/types';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { renderWithQueryClient } from '@/react-tools/test-utils';
import { server, rest } from '@/setup-tests/server';
import { EnvironmentItem } from './EnvironmentItem';
test('loads component', async () => {
const env: Environment = {
TagIds: [],
GroupId: 1,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
const { getByText } = renderComponent(env);
expect(getByText(env.Name)).toBeInTheDocument();
@ -34,19 +23,8 @@ test('shows group name', async () => {
const groupName = 'group-name';
const groupId: EnvironmentGroupId = 14;
const env: Environment = {
TagIds: [],
GroupId: groupId,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
env.GroupId = groupId;
const { findByText } = renderComponent(env, { Name: groupName });

View File

@ -69,10 +69,8 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
<span className="space-left blocklist-item-subtitle">
{isEdge ? (
<EdgeIndicator
edgeId={environment.EdgeID}
checkInInterval={environment.EdgeCheckinInterval}
lastCheckInDate={environment.LastCheckInDate}
queryDate={environment.QueryDate}
environment={environment}
showLastCheckInDate
/>
) : (
<>

View File

@ -17,9 +17,13 @@ export function usePublicSettings() {
});
}
export function useSettings<T = Settings>(select?: (settings: Settings) => T) {
export function useSettings<T = Settings>(
select?: (settings: Settings) => T,
enabled?: boolean
) {
return useQuery(['settings'], getSettings, {
select,
enabled,
meta: {
error: {
title: 'Failure',

View File

@ -125,4 +125,10 @@ export interface Settings {
AllowStackManagementForRegularUsers: boolean;
AllowDeviceMappingForRegularUsers: boolean;
AllowContainerCapabilitiesForRegularUsers: boolean;
Edge: {
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
AsyncMode: boolean;
};
}

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import { Team } from '@/portainer/teams/types';
import { Role, User, UserId } from '@/portainer/users/types';
import { Environment } from '@/portainer/environments/types';
export function createMockUsers(
count: number,
@ -59,3 +60,25 @@ export function createMockResourceGroups(subscription: string, count: number) {
return { value: resourceGroups };
}
export function createMockEnvironment(): Environment {
return {
TagIds: [],
GroupId: 1,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
EdgeKey: '',
Id: 3,
UserTrusted: false,
Edge: {
AsyncMode: false,
PingInterval: 0,
CommandInterval: 0,
SnapshotInterval: 0,
},
};
}

View File

@ -4,7 +4,7 @@ import { Widget, WidgetBody } from './Widget';
import { Button } from './buttons';
interface Props {
title: string;
title?: string;
onDismiss?(): void;
bodyClassName?: string;
wrapperStyle?: Record<string, string>;
@ -23,21 +23,23 @@ export function InformationPanel({
<Widget>
<WidgetBody className={bodyClassName}>
<div style={wrapperStyle}>
<div className="col-sm-12 form-section-title">
<span style={{ float: 'left' }}>{title}</span>
{!!onDismiss && (
<span
className="small"
style={{ float: 'right' }}
ng-if="dismissAction"
>
<Button color="link" onClick={() => onDismiss()}>
<i className="fa fa-times" /> dismiss
</Button>
</span>
)}
</div>
<div className="form-group">{children}</div>
{title && (
<div className="col-sm-12 form-section-title">
<span style={{ float: 'left' }}>{title}</span>
{!!onDismiss && (
<span
className="small"
style={{ float: 'right' }}
ng-if="dismissAction"
>
<Button color="link" onClick={() => onDismiss()}>
<i className="fa fa-times" /> dismiss
</Button>
</span>
)}
</div>
)}
<div>{children}</div>
</div>
</WidgetBody>
</Widget>

View File

@ -66,12 +66,7 @@ export function WizardEndpointsList({ environmentIds }: Props) {
</div>
{isEdgeEnvironment(environment.Type) && (
<div className={styles.wizardListEdgeStatus}>
<EdgeIndicator
edgeId={environment.EdgeID}
checkInInterval={environment.EdgeCheckinInterval}
queryDate={environment.QueryDate}
lastCheckInDate={environment.LastCheckInDate}
/>
<EdgeIndicator environment={environment} />
</div>
)}
</div>