mirror of https://github.com/portainer/portainer
fix(edge): show heartbeat for async env [EE-3380] (#7097)
parent
60cd7b5527
commit
825269c119
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -125,4 +125,10 @@ export interface Settings {
|
|||
AllowStackManagementForRegularUsers: boolean;
|
||||
AllowDeviceMappingForRegularUsers: boolean;
|
||||
AllowContainerCapabilitiesForRegularUsers: boolean;
|
||||
Edge: {
|
||||
PingInterval: number;
|
||||
SnapshotInterval: number;
|
||||
CommandInterval: number;
|
||||
AsyncMode: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue