diff --git a/app/docker/components/datatables/nodes-datatable/nodesDatatable.html b/app/docker/components/datatables/nodes-datatable/nodesDatatable.html
deleted file mode 100644
index 645db63f8..000000000
--- a/app/docker/components/datatables/nodes-datatable/nodesDatatable.html
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
-
- {{ item.Name || item.Hostname }}
- {{ item.Name || item.Hostname }}
- |
- {{ item.Role }} |
- {{ item.CPUs / 1000000000 }} |
- {{ item.Memory | humansize }} |
- {{ item.EngineVersion }} |
- {{ item.Addr }} |
- {{ item.Status }} |
- {{ item.Availability }} |
-
-
- Loading... |
-
-
- No node available. |
-
-
-
-
-
-
-
-
diff --git a/app/docker/components/datatables/nodes-datatable/nodesDatatable.js b/app/docker/components/datatables/nodes-datatable/nodesDatatable.js
deleted file mode 100644
index 1b6de9fde..000000000
--- a/app/docker/components/datatables/nodes-datatable/nodesDatatable.js
+++ /dev/null
@@ -1,15 +0,0 @@
-angular.module('portainer.docker').component('nodesDatatable', {
- templateUrl: './nodesDatatable.html',
- controller: 'GenericDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- dataset: '<',
- tableKey: '@',
- orderBy: '@',
- reverseOrder: '<',
- showIpAddressColumn: '<',
- accessToNodeDetails: '<',
- refreshCallback: '<',
- },
-});
diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js
index 4f5944e25..9839394d9 100644
--- a/app/docker/filters/filters.js
+++ b/app/docker/filters/filters.js
@@ -1,5 +1,5 @@
import _ from 'lodash-es';
-import { joinCommand, taskStatusBadge, nodeStatusBadge, trimSHA } from './utils';
+import { joinCommand, taskStatusBadge, nodeStatusBadge, trimSHA, dockerNodeAvailabilityBadge } from './utils';
function includeString(text, values) {
return values.some(function (val) {
@@ -76,17 +76,7 @@ angular
};
})
.filter('nodestatusbadge', () => nodeStatusBadge)
- .filter('dockerNodeAvailabilityBadge', function () {
- 'use strict';
- return function (text) {
- if (text === 'pause') {
- return 'warning';
- } else if (text === 'drain') {
- return 'danger';
- }
- return 'success';
- };
- })
+ .filter('dockerNodeAvailabilityBadge', () => dockerNodeAvailabilityBadge)
.filter('trimcontainername', function () {
'use strict';
return function (name) {
diff --git a/app/docker/filters/utils.ts b/app/docker/filters/utils.ts
index 144df280e..05723b239 100644
--- a/app/docker/filters/utils.ts
+++ b/app/docker/filters/utils.ts
@@ -1,4 +1,4 @@
-import { NodeStatus, TaskState } from 'docker-types/generated/1.41';
+import { NodeStatus, TaskState, NodeSpec } from 'docker-types/generated/1.41';
import _ from 'lodash';
export function trimSHA(imageName: string) {
@@ -61,3 +61,15 @@ export function nodeStatusBadge(text: NodeStatus['State']) {
return 'success';
}
+
+export function dockerNodeAvailabilityBadge(text: NodeSpec['Availability']) {
+ if (text === 'pause') {
+ return 'warning';
+ }
+
+ if (text === 'drain') {
+ return 'danger';
+ }
+
+ return 'success';
+}
diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts
index 4e466be12..bbf37a899 100644
--- a/app/docker/react/components/index.ts
+++ b/app/docker/react/components/index.ts
@@ -28,12 +28,14 @@ import { StacksDatatable } from '@/react/docker/stacks/ListView/StacksDatatable'
import { containersModule } from './containers';
import { servicesModule } from './services';
import { networksModule } from './networks';
+import { swarmModule } from './swarm';
const ngModule = angular
.module('portainer.docker.react.components', [
containersModule,
servicesModule,
networksModule,
+ swarmModule,
])
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
diff --git a/app/docker/react/components/swarm.ts b/app/docker/react/components/swarm.ts
new file mode 100644
index 000000000..b96f9a6f8
--- /dev/null
+++ b/app/docker/react/components/swarm.ts
@@ -0,0 +1,17 @@
+import angular from 'angular';
+
+import { r2a } from '@/react-tools/react2angular';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { NodesDatatable } from '@/react/docker/swarm/SwarmView/NodesDatatable';
+
+export const swarmModule = angular
+ .module('portainer.docker.react.components.swarm', [])
+ .component(
+ 'nodesDatatable',
+ r2a(withUIRouter(NodesDatatable), [
+ 'dataset',
+ 'isIpColumnVisible',
+ 'haveAccessToNode',
+ 'onRefresh',
+ ])
+ ).name;
diff --git a/app/docker/views/swarm/swarm.html b/app/docker/views/swarm/swarm.html
index cc577b785..0aab190b9 100644
--- a/app/docker/views/swarm/swarm.html
+++ b/app/docker/views/swarm/swarm.html
@@ -37,17 +37,4 @@
-
+
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx b/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx
index fd071f209..7a3f7e8ac 100644
--- a/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx
+++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx
@@ -9,7 +9,7 @@ import { mergeOptions } from '@@/datatables/extend-options/mergeOptions';
import { withMeta } from '@@/datatables/extend-options/withMeta';
import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected';
-import { useColumns } from './columns';
+import { useColumns } from './useColumns';
const tableKey = 'macvlan-nodes-selector';
const store = createPersistedStore(tableKey);
@@ -41,7 +41,7 @@ export function MacvlanNodesSelector({
settingsManager={tableState}
extendTableOptions={mergeOptions(
withMeta({
- table: 'macvlan-nodes',
+ table: 'nodes',
haveAccessToNode,
}),
withControlledSelected(
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts
deleted file mode 100644
index cbbf34268..000000000
--- a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import _ from 'lodash';
-
-import { columnHelper } from './column-helper';
-import { name } from './name';
-import { status } from './status';
-
-export function useColumns(isIpColumnVisible: boolean) {
- return _.compact([
- name,
- columnHelper.accessor('Role', {}),
- columnHelper.accessor('EngineVersion', {
- header: 'Engine',
- }),
- isIpColumnVisible &&
- columnHelper.accessor('Addr', {
- header: 'IP Address',
- }),
- status,
- ]);
-}
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/useColumns.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/useColumns.ts
new file mode 100644
index 000000000..0279e251d
--- /dev/null
+++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/useColumns.ts
@@ -0,0 +1,17 @@
+import { useMemo } from 'react';
+import _ from 'lodash';
+
+import {
+ engine,
+ ip,
+ role,
+ name,
+ status,
+} from '@/react/docker/swarm/SwarmView/NodesDatatable/columns';
+
+export function useColumns(isIpColumnVisible: boolean) {
+ return useMemo(
+ () => _.compact([name, role, engine, isIpColumnVisible && ip, status]),
+ [isIpColumnVisible]
+ );
+}
diff --git a/app/react/docker/swarm/SwarmView/NodesDatatable/NodesDatatable.tsx b/app/react/docker/swarm/SwarmView/NodesDatatable/NodesDatatable.tsx
new file mode 100644
index 000000000..e339f991a
--- /dev/null
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/NodesDatatable.tsx
@@ -0,0 +1,70 @@
+import { Trello } from 'lucide-react';
+
+import { NodeViewModel } from '@/docker/models/node';
+
+import { Datatable, TableSettingsMenu } from '@@/datatables';
+import {
+ BasicTableSettings,
+ RefreshableTableSettings,
+ createPersistedStore,
+ refreshableSettings,
+} from '@@/datatables/types';
+import { useTableState } from '@@/datatables/useTableState';
+import { useRepeater } from '@@/datatables/useRepeater';
+import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
+import { withMeta } from '@@/datatables/extend-options/withMeta';
+
+import { useColumns } from './columns';
+
+const tableKey = 'nodes';
+
+interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
+
+const store = createPersistedStore(
+ tableKey,
+ undefined,
+ (set) => ({
+ ...refreshableSettings(set),
+ })
+);
+
+export function NodesDatatable({
+ dataset,
+ isIpColumnVisible,
+ haveAccessToNode,
+ onRefresh,
+}: {
+ dataset?: Array;
+ isIpColumnVisible: boolean;
+ haveAccessToNode: boolean;
+ onRefresh(): Promise;
+}) {
+ const columns = useColumns(isIpColumnVisible);
+ const tableState = useTableState(store, tableKey);
+ useRepeater(tableState.autoRefreshRate, onRefresh);
+
+ return (
+
+ disableSelect
+ title="Nodes"
+ titleIcon={Trello}
+ columns={columns}
+ dataset={dataset || []}
+ isLoading={!dataset}
+ emptyContentLabel="No node available"
+ settingsManager={tableState}
+ extendTableOptions={withMeta({
+ table: 'nodes',
+ haveAccessToNode,
+ })}
+ renderTableSettings={() => (
+
+ tableState.setAutoRefreshRate(value)}
+ />
+
+ )}
+ />
+ );
+}
diff --git a/app/react/docker/swarm/SwarmView/NodesDatatable/columns/availability.tsx b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/availability.tsx
new file mode 100644
index 000000000..0da94e91a
--- /dev/null
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/availability.tsx
@@ -0,0 +1,29 @@
+import clsx from 'clsx';
+
+import { NodeViewModel } from '@/docker/models/node';
+
+import { columnHelper } from './column-helper';
+
+export const availability = columnHelper.accessor('Availability', {
+ header: 'Availability',
+ cell({ getValue }) {
+ const value = getValue();
+ return (
+
+ {value}
+
+ );
+ },
+});
+
+export function badgeClass(text: NodeViewModel['Availability']) {
+ if (text === 'pause') {
+ return 'warning';
+ }
+
+ if (text === 'drain') {
+ return 'danger';
+ }
+
+ return 'success';
+}
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/column-helper.ts
similarity index 100%
rename from app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts
rename to app/react/docker/swarm/SwarmView/NodesDatatable/columns/column-helper.ts
diff --git a/app/react/docker/swarm/SwarmView/NodesDatatable/columns/index.ts b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/index.ts
new file mode 100644
index 000000000..0844321a2
--- /dev/null
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/index.ts
@@ -0,0 +1,53 @@
+import _ from 'lodash';
+import { useMemo } from 'react';
+
+import { humanize } from '@/portainer/filters/filters';
+
+import { columnHelper } from './column-helper';
+import { name } from './name';
+import { status } from './status';
+import { availability } from './availability';
+
+export { name, status };
+
+export const role = columnHelper.accessor('Role', {});
+
+export const engine = columnHelper.accessor('EngineVersion', {
+ header: 'Engine',
+});
+
+export const ip = columnHelper.accessor('Addr', {
+ header: 'IP Address',
+});
+
+export const cpu = columnHelper.accessor(
+ (item) => (item.CPUs ? item.CPUs / 1000000000 : 0),
+ {
+ header: 'CPU',
+ }
+);
+
+export const memory = columnHelper.accessor('Memory', {
+ header: 'Memory',
+ cell({ getValue }) {
+ const value = getValue();
+ return humanize(value);
+ },
+});
+
+export function useColumns(isIpColumnVisible: boolean) {
+ return useMemo(
+ () =>
+ _.compact([
+ name,
+ role,
+ cpu,
+ memory,
+ engine,
+ isIpColumnVisible && ip,
+ status,
+ availability,
+ ]),
+ [isIpColumnVisible]
+ );
+}
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/name.tsx
similarity index 100%
rename from app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx
rename to app/react/docker/swarm/SwarmView/NodesDatatable/columns/name.tsx
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/status.tsx
similarity index 92%
rename from app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx
rename to app/react/docker/swarm/SwarmView/NodesDatatable/columns/status.tsx
index 88642d7ac..a61e35bca 100644
--- a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/columns/status.tsx
@@ -5,7 +5,7 @@ import { nodeStatusBadge } from '@/docker/filters/utils';
import { columnHelper } from './column-helper';
export const status = columnHelper.accessor('Status', {
- cell: ({ getValue }) => {
+ cell({ getValue }) {
const value = getValue();
return (
diff --git a/app/react/docker/swarm/SwarmView/NodesDatatable/index.ts b/app/react/docker/swarm/SwarmView/NodesDatatable/index.ts
new file mode 100644
index 000000000..f5e6969ba
--- /dev/null
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/index.ts
@@ -0,0 +1 @@
+export { NodesDatatable } from './NodesDatatable';
diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts b/app/react/docker/swarm/SwarmView/NodesDatatable/types.ts
similarity index 79%
rename from app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts
rename to app/react/docker/swarm/SwarmView/NodesDatatable/types.ts
index 290c64a5d..c5f14029d 100644
--- a/app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts
+++ b/app/react/docker/swarm/SwarmView/NodesDatatable/types.ts
@@ -3,12 +3,12 @@ import { TableMeta as BaseTableMeta } from '@tanstack/react-table';
import { NodeViewModel } from '@/docker/models/node';
export type TableMeta = BaseTableMeta & {
- table: 'macvlan-nodes';
+ table: 'nodes';
haveAccessToNode: boolean;
};
export function isTableMeta(
meta?: BaseTableMeta
): meta is TableMeta {
- return !!meta && meta.table === 'macvlan-nodes';
+ return !!meta && meta.table === 'nodes';
}