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)}
+
+ );
+}