diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index 5ebe986fb..ab05117ef 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -46,8 +46,6 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
}
-
- await KubernetesNamespaceService.get();
} catch (e) {
let params = {};
diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts
index ac2509366..3d5a8b8d8 100644
--- a/app/kubernetes/react/views/index.ts
+++ b/app/kubernetes/react/views/index.ts
@@ -6,6 +6,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable';
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
+import { DashboardView } from '@/react/kubernetes/DashboardView';
import { ServicesView } from '@/react/kubernetes/ServicesView';
export const viewsModule = angular
@@ -24,4 +25,8 @@ export const viewsModule = angular
.component(
'kubernetesIngressesCreateView',
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), [])
+ )
+ .component(
+ 'kubernetesDashboardView',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
).name;
diff --git a/app/kubernetes/views/dashboard/dashboard.html b/app/kubernetes/views/dashboard/dashboard.html
deleted file mode 100644
index a076c70a3..000000000
--- a/app/kubernetes/views/dashboard/dashboard.html
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Environment |
-
- {{ ctrl.endpoint.Name }}
- |
-
-
- URL |
- {{ ctrl.endpoint.URL | stripprotocol }} |
-
-
- Tags |
- {{ ctrl.endpointTags }} |
-
-
-
-
-
-
-
-
-
-
diff --git a/app/kubernetes/views/dashboard/dashboard.js b/app/kubernetes/views/dashboard/dashboard.js
deleted file mode 100644
index c2ada72c2..000000000
--- a/app/kubernetes/views/dashboard/dashboard.js
+++ /dev/null
@@ -1,8 +0,0 @@
-angular.module('portainer.kubernetes').component('kubernetesDashboardView', {
- templateUrl: './dashboard.html',
- controller: 'KubernetesDashboardController',
- controllerAs: 'ctrl',
- bindings: {
- endpoint: '<',
- },
-});
diff --git a/app/kubernetes/views/dashboard/dashboardController.js b/app/kubernetes/views/dashboard/dashboardController.js
deleted file mode 100644
index f080f949c..000000000
--- a/app/kubernetes/views/dashboard/dashboardController.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import angular from 'angular';
-import _ from 'lodash-es';
-import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
-import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
-import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
-
-class KubernetesDashboardController {
- /* @ngInject */
- constructor(
- $async,
- Notifications,
- EndpointService,
- KubernetesResourcePoolService,
- KubernetesApplicationService,
- KubernetesConfigurationService,
- KubernetesVolumeService,
- Authentication,
- TagService
- ) {
- this.$async = $async;
- this.Notifications = Notifications;
- this.EndpointService = EndpointService;
- this.KubernetesResourcePoolService = KubernetesResourcePoolService;
- this.KubernetesApplicationService = KubernetesApplicationService;
- this.KubernetesConfigurationService = KubernetesConfigurationService;
- this.KubernetesVolumeService = KubernetesVolumeService;
- this.Authentication = Authentication;
- this.TagService = TagService;
-
- this.onInit = this.onInit.bind(this);
- this.getAll = this.getAll.bind(this);
- this.getAllAsync = this.getAllAsync.bind(this);
- }
-
- async getAllAsync() {
- const isAdmin = this.Authentication.isAdmin();
- const storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses;
- this.showEnvUrl = this.endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && this.endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment;
-
- try {
- const [pools, applications, configurations, volumes, tags] = await Promise.all([
- this.KubernetesResourcePoolService.get(),
- this.KubernetesApplicationService.get(),
- this.KubernetesConfigurationService.get(),
- this.KubernetesVolumeService.get(undefined, storageClasses),
- this.TagService.tags(),
- ]);
- this.applications = applications;
- this.volumes = volumes;
-
- this.endpointTags = this.endpoint.TagIds.length
- ? _.join(
- _.filter(
- _.map(this.endpoint.TagIds, (id) => {
- const tag = tags.find((tag) => tag.Id === id);
- return tag ? tag.Name : '';
- }),
- Boolean
- ),
- ', '
- )
- : '-';
-
- if (!isAdmin) {
- this.pools = _.filter(pools, (pool) => !KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name));
- this.configurations = _.filter(configurations, (config) => !KubernetesConfigurationHelper.isSystemToken(config));
- } else {
- this.pools = pools;
- this.configurations = configurations;
- }
- } catch (err) {
- this.Notifications.error('Failure', err, 'Unable to load dashboard data');
- }
- }
-
- getAll() {
- return this.$async(this.getAllAsync);
- }
-
- async onInit() {
- this.state = {
- viewReady: false,
- };
-
- await this.getAll();
-
- this.state.viewReady = true;
- }
-
- $onInit() {
- return this.$async(this.onInit);
- }
-}
-
-export default KubernetesDashboardController;
-angular.module('portainer.kubernetes').controller('KubernetesDashboardController', KubernetesDashboardController);
diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts
index 17f6d4916..45124cf47 100644
--- a/app/portainer/react/components/index.ts
+++ b/app/portainer/react/components/index.ts
@@ -121,7 +121,17 @@ export const componentsModule = angular
.component('reactQueryDevTools', r2a(ReactQueryDevtoolsWrapper, []))
.component(
'dashboardItem',
- r2a(DashboardItem, ['icon', 'type', 'value', 'children'])
+ r2a(DashboardItem, [
+ 'icon',
+ 'type',
+ 'value',
+ 'to',
+ 'children',
+ 'pluralType',
+ 'isLoading',
+ 'isRefetching',
+ 'dataCy',
+ ])
)
.component(
'datatableSearchbar',
diff --git a/app/portainer/tags/queries.ts b/app/portainer/tags/queries.ts
index dd730a3f3..b7ddff481 100644
--- a/app/portainer/tags/queries.ts
+++ b/app/portainer/tags/queries.ts
@@ -5,6 +5,7 @@ import {
withError,
withInvalidate,
} from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
import { createTag, getTags } from './tags.service';
import { Tag, TagId } from './types';
@@ -24,6 +25,14 @@ export function useTags
({
});
}
+export function useTagsForEnvironment(environmentId: EnvironmentId) {
+ const { data: tags, isLoading } = useTags({
+ select: (tags) => tags.filter((tag) => tag.Endpoints[environmentId]),
+ });
+
+ return { tags, isLoading };
+}
+
export function useCreateTagMutation() {
const queryClient = useQueryClient();
diff --git a/app/portainer/tags/types.ts b/app/portainer/tags/types.ts
index f7c0de9a0..3440871e4 100644
--- a/app/portainer/tags/types.ts
+++ b/app/portainer/tags/types.ts
@@ -3,4 +3,5 @@ export type TagId = number;
export interface Tag {
ID: TagId;
Name: string;
+ Endpoints: Record;
}
diff --git a/app/react/azure/DashboardView/DashboardView.tsx b/app/react/azure/DashboardView/DashboardView.tsx
index 2cc1a11f7..6815e9ae3 100644
--- a/app/react/azure/DashboardView/DashboardView.tsx
+++ b/app/react/azure/DashboardView/DashboardView.tsx
@@ -34,12 +34,15 @@ export function DashboardView() {
{!resourceGroupsQuery.isError && !resourceGroupsQuery.isLoading && (
diff --git a/app/react/components/DashboardItem/DashboardGrid.css b/app/react/components/DashboardItem/DashboardGrid.css
deleted file mode 100644
index a2f844a59..000000000
--- a/app/react/components/DashboardItem/DashboardGrid.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.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
index 349d5a217..bf2b95472 100644
--- a/app/react/components/DashboardItem/DashboardGrid.tsx
+++ b/app/react/components/DashboardItem/DashboardGrid.tsx
@@ -1,7 +1,5 @@
import { PropsWithChildren } from 'react';
-import './DashboardGrid.css';
-
export function DashboardGrid({ children }: PropsWithChildren) {
- return {children}
;
+ return {children}
;
}
diff --git a/app/react/components/DashboardItem/DashboardItem.tsx b/app/react/components/DashboardItem/DashboardItem.tsx
index 5a2492bbe..bf2107b3c 100644
--- a/app/react/components/DashboardItem/DashboardItem.tsx
+++ b/app/react/components/DashboardItem/DashboardItem.tsx
@@ -1,25 +1,62 @@
import { ReactNode } from 'react';
import clsx from 'clsx';
+import { Loader2 } from 'lucide-react';
import { Icon, IconProps } from '@/react/components/Icon';
import { pluralize } from '@/portainer/helpers/strings';
+import { Link } from '@@/Link';
+
interface Props extends IconProps {
- value?: number;
type: string;
+ pluralType?: string; // in case the pluralise function isn't suitable
+ isLoading?: boolean;
+ isRefetching?: boolean;
+ value?: number;
+ to?: string;
children?: ReactNode;
+ dataCy?: string;
}
-export function DashboardItem({ value, icon, type, children }: Props) {
- return (
+export function DashboardItem({
+ icon,
+ type,
+ pluralType,
+ isLoading,
+ isRefetching,
+ value,
+ to,
+ children,
+ dataCy,
+}: Props) {
+ const Item = (
+
+ Refreshing total
+
+
+
+ Loading total
+
+
- {typeof value !== 'undefined' ? value : '-'}
+ {typeof value === 'undefined' ? '-' : value}
- {pluralize(value || 0, type)}
+ {pluralize(value || 0, type, pluralType)}
@@ -61,4 +98,13 @@ export function DashboardItem({ value, icon, type, children }: Props) {
+ {Item}
+
+ );
+ }
+ return Item;
}
diff --git a/app/react/components/PageHeader/PageHeader.module.css b/app/react/components/PageHeader/PageHeader.module.css
deleted file mode 100644
index bc4eb823d..000000000
--- a/app/react/components/PageHeader/PageHeader.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.reloadButton {
- padding: 0;
- margin: 0;
-}
diff --git a/app/react/components/PageHeader/PageHeader.tsx b/app/react/components/PageHeader/PageHeader.tsx
index bd38f7945..c2f854ad3 100644
--- a/app/react/components/PageHeader/PageHeader.tsx
+++ b/app/react/components/PageHeader/PageHeader.tsx
@@ -7,7 +7,6 @@ import { Breadcrumbs } from './Breadcrumbs';
import { Crumb } from './Breadcrumbs/Breadcrumbs';
import { HeaderContainer } from './HeaderContainer';
import { HeaderTitle } from './HeaderTitle';
-import styles from './PageHeader.module.css';
interface Props {
id?: string;
@@ -42,7 +41,7 @@ export function PageHeader({
color="none"
size="large"
onClick={onClickedRefresh}
- className={styles.reloadButton}
+ className="m-0 p-0 focus:text-inherit"
disabled={loading}
>
diff --git a/app/react/components/PageHeader/UserMenu.tsx b/app/react/components/PageHeader/UserMenu.tsx
index b6e1de2a5..0e7fe48ef 100644
--- a/app/react/components/PageHeader/UserMenu.tsx
+++ b/app/react/components/PageHeader/UserMenu.tsx
@@ -8,6 +8,7 @@ import { UISrefProps, useSref } from '@uirouter/react';
import clsx from 'clsx';
import { User, ChevronDown } from 'lucide-react';
+import { queryClient } from '@/react-tools/react-query';
import { AutomationTestingProps } from '@/types';
import { useUser } from '@/react/hooks/useUser';
@@ -78,7 +79,10 @@ function MenuLink({
return (