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

feat(home): change layout of env tile [EE-4479] (#8061)

This commit is contained in:
Chaim Lev-Ari 2022-12-07 16:51:20 +02:00 committed by GitHub
parent b48aa1274d
commit eba5879ec8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 717 additions and 445 deletions

View file

@ -1,9 +1,10 @@
import clsx from 'clsx';
import { Activity } from 'lucide-react';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat';
import { Environment } from '@/react/portainer/environments/types';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { PublicSettingsViewModel } from '@/portainer/models/settings';
import { EnvironmentStatusBadgeItem } from './EnvironmentStatusBadgeItem';
interface Props {
showLastCheckInDate?: boolean;
@ -15,100 +16,46 @@ export function EdgeIndicator({
showLastCheckInDate = false,
}: Props) {
const associated = !!environment.EdgeID;
const isValid = useHasHeartbeat(environment, associated);
const isValid = useHasHeartbeat(environment);
if (isValid === null) {
return null;
}
const associated = !!environment.EdgeID;
if (!associated) {
return (
<span role="status" aria-label="edge-status">
<span className="label label-default" aria-label="unassociated">
<EnvironmentStatusBadgeItem aria-label="unassociated">
<s>associated</s>
</span>
</EnvironmentStatusBadgeItem>
</span>
);
}
return (
<span role="status" aria-label="edge-status">
<span
className={clsx('label', {
'label-danger': !isValid,
'label-success': isValid,
})}
<span
role="status"
aria-label="edge-status"
className="flex items-center gap-1"
>
<EnvironmentStatusBadgeItem
color={isValid ? 'success' : 'danger'}
aria-label="edge-heartbeat"
>
heartbeat
</span>
</EnvironmentStatusBadgeItem>
{showLastCheckInDate && !!environment.LastCheckInDate && (
<span
className="space-left small text-muted"
className="small text-muted vertical-center"
aria-label="edge-last-checkin"
title="Last edge check-in"
>
<Activity className="icon icon-sm space-right" aria-hidden="true" />
{isoDateFromTimestamp(environment.LastCheckInDate)}
</span>
)}
</span>
);
}
function useHasHeartbeat(environment: Environment, associated: boolean) {
const settingsQuery = usePublicSettings({ enabled: 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: PublicSettingsViewModel
) {
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.Edge.CheckinInterval;
}
return environment.EdgeCheckinInterval;
}

View file

@ -0,0 +1,21 @@
import { CheckCircle, XCircle } from 'lucide-react';
import { EnvironmentStatus } from '@/react/portainer/environments/types';
import { EnvironmentStatusBadgeItem } from './EnvironmentStatusBadgeItem';
interface Props {
status: EnvironmentStatus;
}
export function EnvironmentStatusBadge({ status }: Props) {
return status === EnvironmentStatus.Up ? (
<EnvironmentStatusBadgeItem color="success" icon={CheckCircle}>
Up
</EnvironmentStatusBadgeItem>
) : (
<EnvironmentStatusBadgeItem color="danger" icon={XCircle}>
Down
</EnvironmentStatusBadgeItem>
);
}

View file

@ -0,0 +1,48 @@
import clsx from 'clsx';
import { AriaAttributes, PropsWithChildren } from 'react';
import { Icon, IconProps } from '@@/Icon';
export function EnvironmentStatusBadgeItem({
className,
children,
color = 'default',
icon,
...aria
}: PropsWithChildren<
{
className?: string;
color?: 'success' | 'danger' | 'default';
icon?: IconProps['icon'];
} & AriaAttributes
>) {
return (
<span
className={clsx(
'flex items-center gap-1',
'border-2 border-solid rounded',
'w-fit py-px px-1',
'text-xs font-semibold text-gray-7',
{
'border-green-3 bg-green-2': color === 'success',
'border-error-3 bg-error-2': color === 'danger',
},
className
)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...aria}
>
{icon && (
<Icon
icon={icon}
className={clsx({
'!text-green-7': color === 'success',
'!text-error-7': color === 'danger',
})}
/>
)}
{children}
</span>
);
}

View file

@ -0,0 +1,33 @@
import { ComponentProps } from 'react';
import { Button } from './buttons';
import { Link } from './Link';
export function LinkButton({
to,
params,
disabled,
children,
...props
}: ComponentProps<typeof Button> & ComponentProps<typeof Link>) {
const button = (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
size="medium"
disabled={disabled}
>
{children}
</Button>
);
if (disabled) {
return button;
}
return (
<Link to={to} params={params} className="text-inherit hover:no-underline">
{button}
</Link>
);
}

View file

@ -0,0 +1,27 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { Icon, IconProps } from '@/react/components/Icon';
interface Props extends IconProps {
value: string | number;
icon: IconProps['icon'];
iconClass?: string;
}
export function StatsItem({
value,
icon,
children,
iconClass,
}: PropsWithChildren<Props>) {
return (
<span className="flex gap-1 items-center">
<Icon className={clsx('icon icon-sm', iconClass)} icon={icon} />
<span>{value}</span>
{children && (
<span className="ml-1 flex gap-2 items-center">{children}</span>
)}
</span>
);
}