diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 04d56a408..53d3982e5 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -205,7 +205,6 @@ input[type='checkbox'] { .blocklist-item { padding: 10px; margin-bottom: 10px; - cursor: pointer; border: 1px solid var(--border-blocklist); border-radius: 8px; margin-right: 10px; @@ -222,8 +221,9 @@ input[type='checkbox'] { color: var(--text-blocklist-item-selected-color); } -.blocklist-item:hover { +.blocklist-item:not(.blocklist-item-not-interactive):hover { @apply border border-blue-7; + cursor: pointer; background-color: var(--bg-blocklist-hover-color); color: var(--text-blocklist-hover-color); @@ -233,10 +233,6 @@ input[type='checkbox'] { display: flex; } -.blocklist-item-line.endpoint-item { - padding: 4px; -} - .blocklist-item-line { display: flex; justify-content: space-between; @@ -248,24 +244,11 @@ input[type='checkbox'] { max-height: 60px; } -.blocklist-item-logo.endpoint-item { - margin: 10px 4px 0 6px; -} - -.blocklist-item-logo.endpoint-item.azure { - margin: 0 0 0 10px; -} - .blocklist-item-title { font-size: 1.8em; font-weight: bold; } -.blocklist-item-title.endpoint-item { - font-size: 1em; - font-weight: bold; -} - .blocklist-item-subtitle { font-size: 0.9em; padding-right: 1em; @@ -660,7 +643,7 @@ input[type='checkbox'] { margin-right: -50%; } -/*bootbox override*/ +/*bootbox override */ .modal-open { padding-right: 0 !important; } diff --git a/app/assets/css/button.css b/app/assets/css/button.css index 13bab5f1b..d6a598fc5 100644 --- a/app/assets/css/button.css +++ b/app/assets/css/button.css @@ -4,7 +4,7 @@ border-radius: 8px; display: inline-flex; - justify-content: space-around; + justify-content: center; align-items: center; gap: 5px; } diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts index d92b4d5e3..a02cc031e 100644 --- a/app/portainer/users/types.ts +++ b/app/portainer/users/types.ts @@ -1,6 +1,8 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; -export type UserId = number; +import { UserId } from './types/user-id'; + +export { type UserId }; export enum Role { Admin = 1, diff --git a/app/portainer/users/types/user-id.ts b/app/portainer/users/types/user-id.ts new file mode 100644 index 000000000..048cd892f --- /dev/null +++ b/app/portainer/users/types/user-id.ts @@ -0,0 +1 @@ +export type UserId = number; diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 43dacc1aa..07b5a0923 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -78,6 +78,7 @@ export function createMockEnvironment(): Environment { AllowNoneIngressClass: false, }, }, + Nomad: { Snapshots: [] }, EdgeKey: '', Id: 3, UserTrusted: false, diff --git a/app/react/components/EdgeIndicator.tsx b/app/react/components/EdgeIndicator.tsx index 0df4641e9..ce7197279 100644 --- a/app/react/components/EdgeIndicator.tsx +++ b/app/react/components/EdgeIndicator.tsx @@ -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 ( - + associated - + ); } return ( - - + heartbeat - + {showLastCheckInDate && !!environment.LastCheckInDate && ( + )} ); } - -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; -} diff --git a/app/react/components/EnvironmentStatusBadge.tsx b/app/react/components/EnvironmentStatusBadge.tsx new file mode 100644 index 000000000..123c6c456 --- /dev/null +++ b/app/react/components/EnvironmentStatusBadge.tsx @@ -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 ? ( + + Up + + ) : ( + + Down + + ); +} diff --git a/app/react/components/EnvironmentStatusBadgeItem.tsx b/app/react/components/EnvironmentStatusBadgeItem.tsx new file mode 100644 index 000000000..7c54fe161 --- /dev/null +++ b/app/react/components/EnvironmentStatusBadgeItem.tsx @@ -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 ( + + {icon && ( + + )} + + {children} + + ); +} diff --git a/app/react/components/LinkButton.tsx b/app/react/components/LinkButton.tsx new file mode 100644 index 000000000..94a9175ff --- /dev/null +++ b/app/react/components/LinkButton.tsx @@ -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 & ComponentProps) { + const button = ( + + ); + + if (disabled) { + return button; + } + + return ( + + {button} + + ); +} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsItem.tsx b/app/react/components/StatsItem.tsx similarity index 58% rename from app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsItem.tsx rename to app/react/components/StatsItem.tsx index 46a1cd175..68f4861de 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsItem.tsx +++ b/app/react/components/StatsItem.tsx @@ -9,20 +9,19 @@ interface Props extends IconProps { iconClass?: string; } -export function EnvironmentStatsItem({ +export function StatsItem({ value, icon, children, iconClass, }: PropsWithChildren) { return ( - - + + {value} - {children && {children}} + {children && ( + {children} + )} ); } diff --git a/app/react/docker/snapshots/types/index.ts b/app/react/docker/snapshots/types/index.ts new file mode 100644 index 000000000..acb169a4c --- /dev/null +++ b/app/react/docker/snapshots/types/index.ts @@ -0,0 +1,26 @@ +import { DockerContainer } from '@/react/docker/containers/types'; + +export type DockerSnapshotRaw = { + Containers: DockerContainer[]; + SnapshotTime: string; +}; + +export interface DockerSnapshot { + TotalCPU: number; + TotalMemory: number; + NodeCount: number; + ImageCount: number; + VolumeCount: number; + RunningContainerCount: number; + StoppedContainerCount: number; + HealthyContainerCount: number; + UnhealthyContainerCount: number; + Time: number; + StackCount: number; + ServiceCount: number; + Swarm: boolean; + DockerVersion: string; + GpuUseAll: boolean; + GpuUseList: string[]; + SnapshotRaw: DockerSnapshotRaw; +} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx index 53d4f0450..23ea61e3b 100644 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx +++ b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx @@ -1,8 +1,8 @@ import { CellProps, Column } from 'react-table'; +import clsx from 'clsx'; import { Environment } from '@/react/portainer/environments/types'; - -import { EdgeIndicator } from '@@/EdgeIndicator'; +import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat'; export const heartbeat: Column = { Header: 'Heartbeat', @@ -18,3 +18,36 @@ export function StatusCell({ }: CellProps) { return ; } + +function EdgeIndicator({ environment }: { environment: Environment }) { + const isValid = useHasHeartbeat(environment); + + if (isValid === null) { + return null; + } + + const associated = !!environment.EdgeID; + if (!associated) { + return ( + + + associated + + + ); + } + + return ( + + + heartbeat + + + ); +} diff --git a/app/react/edge/hooks/useHasHeartbeat.ts b/app/react/edge/hooks/useHasHeartbeat.ts new file mode 100644 index 000000000..91f17f498 --- /dev/null +++ b/app/react/edge/hooks/useHasHeartbeat.ts @@ -0,0 +1,61 @@ +import { Environment } from '@/react/portainer/environments/types'; +import { usePublicSettings } from '@/react/portainer/settings/queries'; +import { PublicSettingsViewModel } from '@/portainer/models/settings'; + +export function useHasHeartbeat(environment: Environment) { + const associated = !!environment.EdgeID; + + 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; +} diff --git a/app/react/nomad/DashboardView/RunningStatus.tsx b/app/react/nomad/DashboardView/RunningStatus.tsx index c6cbadc58..d86a8d1d1 100644 --- a/app/react/nomad/DashboardView/RunningStatus.tsx +++ b/app/react/nomad/DashboardView/RunningStatus.tsx @@ -1,6 +1,6 @@ import { Power } from 'lucide-react'; -import { Icon } from '@@/Icon'; +import { StatsItem } from '@@/StatsItem'; interface Props { running: number; @@ -11,11 +11,19 @@ export function RunningStatus({ running, stopped }: Props) { return (
- + {`${running || '-'} running`}
- + {`${stopped || '-'} stopped`}
diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx index b073b4107..5bd108ad0 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx @@ -18,9 +18,8 @@ export function AgentVersionTag({ type, version }: Props) { return ( - - +