1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 23:35:31 +02:00

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

This commit is contained in:
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

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