diff --git a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html
deleted file mode 100644
index 522560f91..000000000
--- a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html
+++ /dev/null
@@ -1,209 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
- Actions |
-
-
-
-
- {{ item.PodName }} |
- {{ item.Name }} |
- {{ item.Image | truncate : 64 }} |
- {{ item.ImagePullPolicy }} |
- {{ item.Status }} |
-
-
-
- {{ item.Node }}
-
-
- -
- |
- {{ item.PodIP }} |
- {{ item.CreationDate | getisodate }} |
-
-
- Stats
-
-
- Logs
-
-
- Console
-
- |
-
-
- Loading... |
-
-
- No pod available. |
-
-
-
-
-
-
-
-
diff --git a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js
deleted file mode 100644
index 7e096228b..000000000
--- a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js
+++ /dev/null
@@ -1,14 +0,0 @@
-angular.module('portainer.kubernetes').component('kubernetesContainersDatatable', {
- templateUrl: './containersDatatable.html',
- controller: 'GenericDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- dataset: '<',
- tableKey: '@',
- orderBy: '@',
- refreshCallback: '<',
- isPod: '<',
- useServerMetrics: '<',
- },
-});
diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js
index 6add57116..d4a0f8d43 100644
--- a/app/kubernetes/helpers/application/index.js
+++ b/app/kubernetes/helpers/application/index.js
@@ -76,12 +76,6 @@ class KubernetesApplicationHelper {
return containers;
}
- static associateAllContainersAndApplication(app) {
- const containers = _.flatMap(_.map(app.Pods, 'Containers'));
- KubernetesApplicationHelper.associateContainerPersistedFoldersAndConfigurations(app, containers);
- return containers;
- }
-
static portMappingsFromApplications(applications) {
const res = _.reduce(
applications,
diff --git a/app/kubernetes/pod/filters.js b/app/kubernetes/pod/filters.js
deleted file mode 100644
index 105b561e2..000000000
--- a/app/kubernetes/pod/filters.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import _ from 'lodash-es';
-
-angular
- .module('portainer.kubernetes')
- .filter('kubernetesPodStatusColor', function () {
- 'use strict';
- return function (text) {
- var status = _.toLower(text);
- switch (status) {
- case 'running':
- return 'success';
- case 'waiting':
- return 'warning';
- case 'terminated':
- return 'info';
- default:
- return 'danger';
- }
- };
- })
- .filter('kubernetesPodConditionStatusText', function () {
- 'use strict';
- return function (status, type) {
- switch (type) {
- case 'Unschedulable':
- switch (status) {
- case 'True':
- return 'Alert';
- case 'False':
- return 'OK';
- case 'Unknown':
- return 'Warning';
- }
- break;
- case 'PodScheduled':
- case 'Ready':
- case 'Initialized':
- case 'ContainersReady':
- switch (status) {
- case 'True':
- return 'Ok';
- case 'False':
- return 'Alert';
- case 'Unknown':
- return 'Warning';
- }
- break;
- default:
- return 'Unknown';
- }
- };
- });
diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts
index 21201c221..1ee99dd0c 100644
--- a/app/kubernetes/react/components/index.ts
+++ b/app/kubernetes/react/components/index.ts
@@ -17,6 +17,7 @@ import {
ApplicationDetailsWidget,
ApplicationEventsDatatable,
} from '@/react/kubernetes/applications/DetailsView';
+import { ApplicationContainersDatatable } from '@/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable';
import { withFormValidation } from '@/react-tools/withFormValidation';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { PlacementsDatatable } from '@/react/kubernetes/applications/ItemView/PlacementsDatatable';
@@ -102,6 +103,15 @@ export const ngModule = angular
[]
)
)
+ .component(
+ 'applicationContainersDatatable',
+ r2a(
+ withUIRouter(
+ withReactQuery(withCurrentUser(ApplicationContainersDatatable))
+ ),
+ []
+ )
+ )
.component(
'applicationDetailsWidget',
r2a(
diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html
index 13057f9aa..f14be606d 100644
--- a/app/kubernetes/views/applications/edit/application.html
+++ b/app/kubernetes/views/applications/edit/application.html
@@ -78,17 +78,6 @@
diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js
index 2eac442e3..2771cf7b9 100644
--- a/app/kubernetes/views/applications/edit/applicationController.js
+++ b/app/kubernetes/views/applications/edit/applicationController.js
@@ -10,7 +10,6 @@ import {
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
-import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
@@ -199,7 +198,6 @@ class KubernetesApplicationController {
this.KubernetesNodeService.get(),
]);
this.application = application;
- this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
this.placements = computePlacements(nodes, this.application);
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
@@ -238,7 +236,6 @@ class KubernetesApplicationController {
eventWarningCount: 0,
placementWarning: false,
expandedNote: false,
- useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
publicUrl: this.endpoint.PublicURL,
};
diff --git a/app/react/components/datatables/DatatableContent.tsx b/app/react/components/datatables/DatatableContent.tsx
index be71aca09..d435a919a 100644
--- a/app/react/components/datatables/DatatableContent.tsx
+++ b/app/react/components/datatables/DatatableContent.tsx
@@ -25,7 +25,7 @@ export function DatatableContent>({
const pageRowModel = tableInstance.getPaginationRowModel();
return (
-
+
{headerGroups.map((headerGroup) => (
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
new file mode 100644
index 000000000..16cf04ae9
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
@@ -0,0 +1,111 @@
+import { Server } from 'lucide-react';
+import { useCurrentStateAndParams } from '@uirouter/react';
+import { useMemo } from 'react';
+import { ContainerStatus, Pod } from 'kubernetes-types/core/v1';
+
+import { IndexOptional } from '@/react/kubernetes/configs/types';
+import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
+import { useEnvironment } from '@/react/portainer/environments/queries';
+
+import { Datatable } from '@@/datatables';
+import { useTableState } from '@@/datatables/useTableState';
+
+import { useApplication, useApplicationPods } from '../../application.queries';
+
+import { ContainerRowData } from './types';
+import { getColumns } from './columns';
+
+const storageKey = 'k8sContainersDatatable';
+const settingsStore = createStore(storageKey);
+
+export function ApplicationContainersDatatable() {
+ const tableState = useTableState(settingsStore, storageKey);
+ const {
+ params: {
+ endpointId: environmentId,
+ name,
+ namespace,
+ 'resource-type': resourceType,
+ },
+ } = useCurrentStateAndParams();
+
+ // get the containers from the aapplication pods
+ const { data: application, ...applicationQuery } = useApplication(
+ environmentId,
+ namespace,
+ name,
+ resourceType
+ );
+ const { data: pods, ...podsQuery } = useApplicationPods(
+ environmentId,
+ namespace,
+ name,
+ application
+ );
+ const appContainers = useContainersRowData(pods);
+
+ const { data: isServerMetricsEnabled } = useEnvironment(
+ environmentId,
+ (environment) => !!environment?.Kubernetes?.Configuration.UseServerMetrics
+ );
+
+ return (
+ >
+ dataset={appContainers}
+ columns={getColumns(!!isServerMetricsEnabled)}
+ settingsManager={tableState}
+ isLoading={applicationQuery.isLoading || podsQuery.isLoading}
+ emptyContentLabel="No containers found"
+ title="Application containers"
+ titleIcon={Server}
+ getRowId={(row) => row.name}
+ disableSelect
+ />
+ );
+}
+
+// useContainersRowData row data gets the pod.spec?.containers and pod.spec?.initContainers from an array of pods
+// it then appends the podName, nodeName, podId, creationDate, and status to each container
+function useContainersRowData(pods?: Pod[]): ContainerRowData[] {
+ return (
+ useMemo(
+ () =>
+ pods?.flatMap((pod) => {
+ const containers = [
+ ...(pod.spec?.containers || []),
+ ...(pod.spec?.initContainers || []),
+ ];
+ return containers.map((container) => ({
+ ...container,
+ podName: pod.metadata?.name ?? '',
+ nodeName: pod.spec?.nodeName ?? '',
+ podIp: pod.status?.podIP ?? '',
+ creationDate: pod.status?.startTime ?? '',
+ status: computeContainerStatus(
+ container.name,
+ pod.status?.containerStatuses
+ ),
+ }));
+ }) || [],
+ [pods]
+ ) || []
+ );
+}
+
+function computeContainerStatus(
+ containerName: string,
+ statuses?: ContainerStatus[]
+) {
+ const status = statuses?.find((status) => status.name === containerName);
+ if (!status) {
+ return 'Terminated';
+ }
+ const { state } = status;
+ if (state?.waiting) {
+ return 'Waiting';
+ }
+ if (!state?.running) {
+ return 'Terminated';
+ }
+ return 'Running';
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx
new file mode 100644
index 000000000..dcf596170
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/actions.tsx
@@ -0,0 +1,49 @@
+import { BarChart, FileText, Terminal } from 'lucide-react';
+
+import { Authorized } from '@/react/hooks/useUser';
+
+import { Link } from '@@/Link';
+import { Icon } from '@@/Icon';
+
+import { columnHelper } from './helper';
+
+export function getActions(isServerMetricsEnabled: boolean) {
+ return columnHelper.accessor(() => '', {
+ header: 'Actions',
+ enableSorting: false,
+ cell: ({ row: { original: container } }) => (
+
+ {container.status === 'Running' && isServerMetricsEnabled && (
+
+
+ Stats
+
+ )}
+
+
+ Logs
+
+ {container.status === 'Running' && (
+
+
+
+ Console
+
+
+ )}
+
+ ),
+ });
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/creationDate.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/creationDate.tsx
new file mode 100644
index 000000000..53fb91f29
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/creationDate.tsx
@@ -0,0 +1,11 @@
+import { formatDate } from '@/portainer/filters/filters';
+
+import { columnHelper } from './helper';
+
+export const creationDate = columnHelper.accessor(
+ (row) => formatDate(row.creationDate),
+ {
+ header: 'Creation Date',
+ cell: ({ getValue }) => getValue(),
+ }
+);
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/helper.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/helper.ts
new file mode 100644
index 000000000..aad7ac21e
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/helper.ts
@@ -0,0 +1,5 @@
+import { createColumnHelper } from '@tanstack/react-table';
+
+import { ContainerRowData } from '../types';
+
+export const columnHelper = createColumnHelper();
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/image.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/image.tsx
new file mode 100644
index 000000000..b596c107f
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/image.tsx
@@ -0,0 +1,10 @@
+import { columnHelper } from './helper';
+
+export const image = columnHelper.accessor('image', {
+ header: 'Image',
+ cell: ({ getValue }) => (
+
+ {getValue()}
+
+ ),
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/imagePullPolicy.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/imagePullPolicy.tsx
new file mode 100644
index 000000000..46451a91c
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/imagePullPolicy.tsx
@@ -0,0 +1,6 @@
+import { columnHelper } from './helper';
+
+export const imagePullPolicy = columnHelper.accessor('imagePullPolicy', {
+ header: 'Image Pull Policy',
+ id: 'imagePullPolicy',
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/index.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/index.ts
new file mode 100644
index 000000000..450f08c36
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/index.ts
@@ -0,0 +1,23 @@
+import { pod } from './pod';
+import { name } from './name';
+import { image } from './image';
+import { imagePullPolicy } from './imagePullPolicy';
+import { status } from './status';
+import { node } from './node';
+import { podIp } from './podIp';
+import { creationDate } from './creationDate';
+import { getActions } from './actions';
+
+export function getColumns(isServerMetricsEnabled: boolean) {
+ return [
+ pod,
+ name,
+ image,
+ imagePullPolicy,
+ status,
+ node,
+ podIp,
+ creationDate,
+ getActions(isServerMetricsEnabled),
+ ];
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/name.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/name.tsx
new file mode 100644
index 000000000..db2f4bfdf
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/name.tsx
@@ -0,0 +1,6 @@
+import { columnHelper } from './helper';
+
+export const name = columnHelper.accessor('name', {
+ header: 'Name',
+ id: 'name',
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/node.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/node.tsx
new file mode 100644
index 000000000..d0c9064e7
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/node.tsx
@@ -0,0 +1,24 @@
+import { Authorized } from '@/react/hooks/useUser';
+
+import { Link } from '@@/Link';
+
+import { columnHelper } from './helper';
+
+export const node = columnHelper.accessor('nodeName', {
+ header: 'Node',
+ cell: ({ getValue }) => {
+ const nodeName = getValue();
+ return (
+
+
+
+ {nodeName}
+
+
+
+ );
+ },
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/pod.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/pod.tsx
new file mode 100644
index 000000000..c35863c25
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/pod.tsx
@@ -0,0 +1,10 @@
+import { columnHelper } from './helper';
+
+export const pod = columnHelper.accessor('podName', {
+ header: 'Pod',
+ cell: ({ getValue }) => (
+
+ {getValue()}
+
+ ),
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/podIp.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/podIp.tsx
new file mode 100644
index 000000000..e6fc1c982
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/podIp.tsx
@@ -0,0 +1,6 @@
+import { columnHelper } from './helper';
+
+export const podIp = columnHelper.accessor('podIp', {
+ header: 'Pod IP',
+ id: 'podIp',
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx
new file mode 100644
index 000000000..d181f8377
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/columns/status.tsx
@@ -0,0 +1,29 @@
+import { CellContext } from '@tanstack/react-table';
+
+import { Badge, BadgeType } from '@@/Badge';
+
+import { ContainerRowData } from '../types';
+
+import { columnHelper } from './helper';
+
+export const status = columnHelper.accessor('status', {
+ header: 'Status',
+ cell: StatusCell,
+});
+
+function StatusCell({ getValue }: CellContext) {
+ return {getValue()};
+}
+
+function getContainerStatusType(status: string): BadgeType {
+ switch (status.toLowerCase()) {
+ case 'running':
+ return 'success';
+ case 'waiting':
+ return 'warn';
+ case 'terminated':
+ return 'info';
+ default:
+ return 'danger';
+ }
+}
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/index.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/index.ts
new file mode 100644
index 000000000..52b079c09
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/index.ts
@@ -0,0 +1 @@
+export { ApplicationContainersDatatable } from './ApplicationContainersDatatable';
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts
new file mode 100644
index 000000000..7981ab187
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/types.ts
@@ -0,0 +1,9 @@
+import { Container } from 'kubernetes-types/core/v1';
+
+export interface ContainerRowData extends Container {
+ podName: string;
+ nodeName: string;
+ podIp: string;
+ creationDate: string;
+ status: string;
+}