diff --git a/app/assets/css/icon.css b/app/assets/css/icon.css index 774ee6de4..93ab9fb40 100644 --- a/app/assets/css/icon.css +++ b/app/assets/css/icon.css @@ -89,6 +89,14 @@ pr-icon { stroke: var(--white-color); } +.icon-badge { + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + padding: 1.5%; +} + .icon-nested-gray { height: 30px; width: 30px; diff --git a/app/assets/css/vendor-override.css b/app/assets/css/vendor-override.css index 6affc9333..2dac68221 100644 --- a/app/assets/css/vendor-override.css +++ b/app/assets/css/vendor-override.css @@ -32,12 +32,14 @@ } a { - color: var(--text-link-color); + color: inherit; + cursor: pointer; } a:hover, a:focus { - color: var(--text-link-hover-color); + color: inherit; + text-decoration: none; } .input-group-addon { diff --git a/app/azure/Dashboard/DashboardView.test.tsx b/app/azure/Dashboard/DashboardView.test.tsx index b8e15473b..2df4692be 100644 --- a/app/azure/Dashboard/DashboardView.test.tsx +++ b/app/azure/Dashboard/DashboardView.test.tsx @@ -19,7 +19,7 @@ jest.mock('@uirouter/react', () => ({ test('dashboard items should render correctly', async () => { const { getByLabelText } = await renderComponent(); - const subscriptionsItem = getByLabelText('Subscriptions'); + const subscriptionsItem = getByLabelText('Subscription'); expect(subscriptionsItem).toBeVisible(); const subscriptionElements = within(subscriptionsItem); @@ -31,7 +31,7 @@ test('dashboard items should render correctly', async () => { 'Subscriptions' ); - const resourceGroupsItem = getByLabelText('Resource groups'); + const resourceGroupsItem = getByLabelText('Resource group'); expect(resourceGroupsItem).toBeVisible(); const resourceGroupElements = within(resourceGroupsItem); @@ -47,20 +47,20 @@ test('dashboard items should render correctly', async () => { test('when there are no subscriptions, should show 0 subscriptions and 0 resource groups', async () => { const { getByLabelText } = await renderComponent(); - const subscriptionElements = within(getByLabelText('Subscriptions')); + const subscriptionElements = within(getByLabelText('Subscription')); expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('0'); - const resourceGroupElements = within(getByLabelText('Resource groups')); + const resourceGroupElements = within(getByLabelText('Resource group')); expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('0'); }); test('when there is subscription & resource group data, should display these', async () => { const { getByLabelText } = await renderComponent(1, { 'subscription-1': 2 }); - const subscriptionElements = within(getByLabelText('Subscriptions')); + const subscriptionElements = within(getByLabelText('Subscription')); expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('1'); - const resourceGroupElements = within(getByLabelText('Resource groups')); + const resourceGroupElements = within(getByLabelText('Resource group')); expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('2'); }); @@ -70,7 +70,7 @@ test('should correctly show total number of resource groups across multiple subs 'subscription-2': 3, }); - const resourceGroupElements = within(getByLabelText('Resource groups')); + const resourceGroupElements = within(getByLabelText('Resource group')); expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('5'); }); @@ -81,8 +81,8 @@ test('when only subscriptions fail to load, dont show the dashboard', async () = 500, 200 ); - expect(queryByLabelText('Subscriptions')).not.toBeInTheDocument(); - expect(queryByLabelText('Resource groups')).not.toBeInTheDocument(); + expect(queryByLabelText('Subscription')).not.toBeInTheDocument(); + expect(queryByLabelText('Resource group')).not.toBeInTheDocument(); }); test('when only resource groups fail to load, still show the subscriptions', async () => { @@ -92,8 +92,8 @@ test('when only resource groups fail to load, still show the subscriptions', asy 200, 500 ); - expect(queryByLabelText('Subscriptions')).toBeInTheDocument(); - expect(queryByLabelText('Resource groups')).not.toBeInTheDocument(); + expect(queryByLabelText('Subscription')).toBeInTheDocument(); + expect(queryByLabelText('Resource group')).not.toBeInTheDocument(); }); async function renderComponent( diff --git a/app/azure/Dashboard/DashboardView.tsx b/app/azure/Dashboard/DashboardView.tsx index b60c04517..b3286feb3 100644 --- a/app/azure/Dashboard/DashboardView.tsx +++ b/app/azure/Dashboard/DashboardView.tsx @@ -7,6 +7,7 @@ import { r2a } from '@/react-tools/react2angular'; import { DashboardItem } from '@@/DashboardItem'; import { PageHeader } from '@@/PageHeader'; +import { DashboardGrid } from '@@/DashboardItem/DashboardGrid'; import { useResourceGroups, useSubscriptions } from '../queries'; @@ -53,22 +54,24 @@ export function DashboardView() { <> - {!subscriptionsQuery.isError && ( -
- - {!resourceGroupsQuery.isError && ( +
+ {subscriptionsQuery.data && ( + - )} -
- )} + {!resourceGroupsQuery.isError && !resourceGroupsQuery.isLoading && ( + + )} + + )} +
); } diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js index f3ee255b5..c41006159 100644 --- a/app/docker/filters/filters.js +++ b/app/docker/filters/filters.js @@ -235,49 +235,6 @@ angular return runningTasks; }; }) - .filter('runningcontainers', function () { - 'use strict'; - return function runningContainersFilter(containers) { - return containers.filter(function (container) { - return container.State === 'running'; - }).length; - }; - }) - .filter('stoppedcontainers', function () { - 'use strict'; - return function stoppedContainersFilter(containers) { - return containers.filter(function (container) { - return container.State === 'exited'; - }).length; - }; - }) - .filter('healthycontainers', function () { - 'use strict'; - return function healthyContainersFilter(containers) { - return containers.filter(function (container) { - return container.Status === 'healthy'; - }).length; - }; - }) - .filter('unhealthycontainers', function () { - 'use strict'; - return function unhealthyContainersFilter(containers) { - return containers.filter(function (container) { - return container.Status === 'unhealthy'; - }).length; - }; - }) - .filter('imagestotalsize', function () { - 'use strict'; - return function (images) { - var totalImageSize = 0; - for (var i = 0; i < images.length; i++) { - var item = images[i]; - totalImageSize += item.VirtualSize; - } - return totalImageSize; - }; - }) .filter('tasknodename', function () { 'use strict'; return function (nodeId, nodes) { diff --git a/app/docker/views/dashboard/dashboard.html b/app/docker/views/dashboard/dashboard.html index a249f1597..7b6403ec6 100644 --- a/app/docker/views/dashboard/dashboard.html +++ b/app/docker/views/dashboard/dashboard.html @@ -28,136 +28,74 @@ -
-
- - - - - - - - - - - - - - - - - - - - - -
Environment - {{ endpoint.Name }} - - {{ endpoint.Snapshots[0].TotalCPU }} {{ endpoint.Snapshots[0].TotalMemory | humansize }} - - - - {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }} - + Agent -
URL{{ endpoint.URL | stripprotocol }}
Tags{{ endpointTags }}
- -
-
-
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Environment + {{ endpoint.Name }} + + {{ endpoint.Snapshots[0].TotalCPU }} {{ endpoint.Snapshots[0].TotalMemory | humansize }} + + + - {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }} + + Agent +
URL{{ endpoint.URL | stripprotocol }}
Tags{{ endpointTags }}
+ +
+
+
+
-
-
- diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index 551684afb..7461c1ce8 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -3,6 +3,8 @@ import _ from 'lodash'; import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper'; import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { useContainerStatusComponent } from '@/react/docker/DashboardView/ContainerStatus'; +import { useImagesTotalSizeComponent } from '@/react/docker/DashboardView/ImagesTotalSize'; angular.module('portainer.docker').controller('DashboardController', [ '$scope', @@ -60,7 +62,11 @@ angular.module('portainer.docker').controller('DashboardController', [ }) .then(function success(data) { $scope.containers = data.containers; + $scope.containerStatusComponent = useContainerStatusComponent(data.containers); + $scope.images = data.images; + $scope.imagesTotalSizeComponent = useImagesTotalSizeComponent(imagesTotalSize(data.images)); + $scope.volumeCount = data.volumes.length; $scope.networkCount = data.networks.length; $scope.serviceCount = data.services.length; @@ -94,3 +100,7 @@ angular.module('portainer.docker').controller('DashboardController', [ initView(); }, ]); + +function imagesTotalSize(images) { + return images.reduce((acc, image) => acc + image.VirtualSize, 0); +} diff --git a/app/kubernetes/views/dashboard/dashboard.html b/app/kubernetes/views/dashboard/dashboard.html index 6cf424dc9..649a16147 100644 --- a/app/kubernetes/views/dashboard/dashboard.html +++ b/app/kubernetes/views/dashboard/dashboard.html @@ -31,57 +31,26 @@
-
-
+
+ -
+ + -
+ - diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts index 9bbfef726..da7e5eefd 100644 --- a/app/portainer/react/components/index.ts +++ b/app/portainer/react/components/index.ts @@ -10,6 +10,7 @@ import { Loading } from '@@/Widget/Loading'; import { PasswordCheckHint } from '@@/PasswordCheckHint'; import { ViewLoading } from '@@/ViewLoading'; import { Tooltip } from '@@/Tip/Tooltip'; +import { DashboardItem } from '@@/DashboardItem'; import { fileUploadField } from './file-upload-field'; import { switchField } from './switch-field'; @@ -38,4 +39,8 @@ export const componentsModule = angular 'prIcon', r2a(Icon, ['className', 'feather', 'icon', 'mode', 'size']) ) - .component('reactQueryDevTools', r2a(ReactQueryDevtoolsWrapper, [])).name; + .component('reactQueryDevTools', r2a(ReactQueryDevtoolsWrapper, [])) + .component( + 'dashboardItem', + r2a(DashboardItem, ['featherIcon', 'icon', 'type', 'value', 'children']) + ).name; diff --git a/app/react/components/DashboardItem/DashboardGrid.css b/app/react/components/DashboardItem/DashboardGrid.css new file mode 100644 index 000000000..a2f844a59 --- /dev/null +++ b/app/react/components/DashboardItem/DashboardGrid.css @@ -0,0 +1,3 @@ +.dashboard-grid { + @apply grid grid-cols-2 gap-3; +} diff --git a/app/react/components/DashboardItem/DashboardGrid.tsx b/app/react/components/DashboardItem/DashboardGrid.tsx new file mode 100644 index 000000000..349d5a217 --- /dev/null +++ b/app/react/components/DashboardItem/DashboardGrid.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from 'react'; + +import './DashboardGrid.css'; + +export function DashboardGrid({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/app/react/components/DashboardItem/DashboardItem.stories.tsx b/app/react/components/DashboardItem/DashboardItem.stories.tsx index 805c86d1e..e3d868ca6 100644 --- a/app/react/components/DashboardItem/DashboardItem.stories.tsx +++ b/app/react/components/DashboardItem/DashboardItem.stories.tsx @@ -34,3 +34,11 @@ export function WithLink() { ); } + +export function WithChildren() { + return ( + +
Children
+
+ ); +} diff --git a/app/react/components/DashboardItem/DashboardItem.tsx b/app/react/components/DashboardItem/DashboardItem.tsx index 8da37e438..6f4b50f56 100644 --- a/app/react/components/DashboardItem/DashboardItem.tsx +++ b/app/react/components/DashboardItem/DashboardItem.tsx @@ -1,8 +1,8 @@ import { ReactNode } from 'react'; +import clsx from 'clsx'; import { Icon, IconProps } from '@/react/components/Icon'; - -import { Widget, WidgetBody } from '@@/Widget'; +import { pluralize } from '@/portainer/helpers/strings'; interface Props extends IconProps { value?: number; @@ -18,21 +18,29 @@ export function DashboardItem({ featherIcon, }: Props) { return ( -
- - -
- +
+
+
+ +
+ +
+
+ {typeof value !== 'undefined' ? value : '-'}
-
{children}
-
- {value} +
+ {pluralize(value || 0, type)}
-
- {type} -
- - +
+ +
{children}
+
); } diff --git a/app/react/docker/DashboardView/ContainerStatus.tsx b/app/react/docker/DashboardView/ContainerStatus.tsx new file mode 100644 index 000000000..52209c276 --- /dev/null +++ b/app/react/docker/DashboardView/ContainerStatus.tsx @@ -0,0 +1,52 @@ +import { DockerContainer } from '../containers/types'; + +interface Props { + containers: DockerContainer[]; +} + +export function useContainerStatusComponent(containers: DockerContainer[]) { + return ; +} + +export function ContainerStatus({ containers }: Props) { + return ( + <> +
+
+ + {runningContainersFilter(containers)} running +
+
+ + {stoppedContainersFilter(containers)} stopped +
+
+
+
+ + {healthyContainersFilter(containers)} healthy +
+
+ + {unhealthyContainersFilter(containers)} unhealthy +
+
+ + ); +} + +function runningContainersFilter(containers: DockerContainer[]) { + return containers.filter((container) => container.Status === 'running') + .length; +} +function stoppedContainersFilter(containers: DockerContainer[]) { + return containers.filter((container) => container.Status === 'exited').length; +} +function healthyContainersFilter(containers: DockerContainer[]) { + return containers.filter((container) => container.Status === 'healthy') + .length; +} +function unhealthyContainersFilter(containers: DockerContainer[]) { + return containers.filter((container) => container.Status === 'unhealthy') + .length; +} diff --git a/app/react/docker/DashboardView/ImagesTotalSize.tsx b/app/react/docker/DashboardView/ImagesTotalSize.tsx new file mode 100644 index 000000000..6746b88d0 --- /dev/null +++ b/app/react/docker/DashboardView/ImagesTotalSize.tsx @@ -0,0 +1,18 @@ +import { humanize } from '@/portainer/filters/filters'; + +interface Props { + imagesTotalSize: number; +} + +export function useImagesTotalSizeComponent(imagesTotalSize: number) { + return ; +} + +export function ImagesTotalSize({ imagesTotalSize }: Props) { + return ( +
+ + {humanize(imagesTotalSize)} +
+ ); +}