From d88ef03ddb8b416044a28d233f7edd7bf8107e45 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 8 Apr 2024 13:18:59 +0300 Subject: [PATCH] refactor(edge/jobs): migrate results table to react [EE-4679] (#10663) --- .../edgeJobResultsDatatable.css | 3 - .../edgeJobResultsDatatable.html | 70 ------------------- .../edgeJobResultsDatatableController.js | 30 -------- .../edge-job-results-datatable/index.js | 21 ------ app/edge/react/components/edge-jobs.ts | 18 +++++ app/edge/react/components/index.ts | 4 +- app/edge/views/edge-jobs/edgeJob/edgeJob.html | 14 ++-- .../edge-jobs/edgeJob/edgeJobController.js | 20 ++++-- app/react/edge/edge-jobs/ItemView/.keep | 0 .../ResultsDatatable/ResultsDatatable.tsx | 66 +++++++++++++++++ .../ItemView/ResultsDatatable/columns.tsx | 60 ++++++++++++++++ .../ResultsDatatable/datatable-store.ts | 14 ++++ .../ItemView/ResultsDatatable/types.ts | 34 +++++++++ app/react/edge/edge-jobs/types.ts | 8 ++- 14 files changed, 223 insertions(+), 139 deletions(-) delete mode 100644 app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.css delete mode 100644 app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.html delete mode 100644 app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js delete mode 100644 app/edge/components/edge-job-results-datatable/index.js create mode 100644 app/edge/react/components/edge-jobs.ts delete mode 100644 app/react/edge/edge-jobs/ItemView/.keep create mode 100644 app/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable.tsx create mode 100644 app/react/edge/edge-jobs/ItemView/ResultsDatatable/columns.tsx create mode 100644 app/react/edge/edge-jobs/ItemView/ResultsDatatable/datatable-store.ts create mode 100644 app/react/edge/edge-jobs/ItemView/ResultsDatatable/types.ts diff --git a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.css b/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.css deleted file mode 100644 index 1b98a1e24..000000000 --- a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.css +++ /dev/null @@ -1,3 +0,0 @@ -.edge-job-results-datatable thead th { - width: 50%; -} diff --git a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.html b/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.html deleted file mode 100644 index 3313f55bc..000000000 --- a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.html +++ /dev/null @@ -1,70 +0,0 @@ -
- - -
-
- - {{ $ctrl.titleText }} -
- -
-
- - - - - - - - - - - - - - - - - - - -
- - Environment - - - - Actions
- {{ item.Endpoint.Name }} - - - - - Logs marked for collection, please wait until the logs are available. -
Loading...
No result available.
-
- -
-
-
diff --git a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js b/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js deleted file mode 100644 index 92217e3cd..000000000 --- a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js +++ /dev/null @@ -1,30 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash-es'; - -export class EdgeJobResultsDatatableController { - /* @ngInject */ - constructor($controller, $scope, $state) { - this.$state = $state; - angular.extend(this, $controller('GenericDatatableController', { $scope })); - } - - collectLogs(...args) { - this.settings.repeater.autoRefresh = true; - this.settings.repeater.refreshRate = '5'; - this.onSettingsRepeaterChange(); - this.onCollectLogsClick(...args); - } - - $onChanges({ dataset }) { - if (dataset && dataset.currentValue) { - this.onDatasetChange(dataset.currentValue); - } - } - - onDatasetChange(dataset) { - const anyCollecting = _.some(dataset, (item) => item.LogsStatus === 2); - this.settings.repeater.autoRefresh = anyCollecting; - this.settings.repeater.refreshRate = '5'; - this.onSettingsRepeaterChange(); - } -} diff --git a/app/edge/components/edge-job-results-datatable/index.js b/app/edge/components/edge-job-results-datatable/index.js deleted file mode 100644 index 132690633..000000000 --- a/app/edge/components/edge-job-results-datatable/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import angular from 'angular'; - -import { EdgeJobResultsDatatableController } from './edgeJobResultsDatatableController'; -import './edgeJobResultsDatatable.css'; - -angular.module('portainer.edge').component('edgeJobResultsDatatable', { - templateUrl: './edgeJobResultsDatatable.html', - controller: EdgeJobResultsDatatableController, - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - onDownloadLogsClick: '<', - onCollectLogsClick: '<', - onClearLogsClick: '<', - refreshCallback: '<', - }, -}); diff --git a/app/edge/react/components/edge-jobs.ts b/app/edge/react/components/edge-jobs.ts new file mode 100644 index 000000000..0b88bc3d7 --- /dev/null +++ b/app/edge/react/components/edge-jobs.ts @@ -0,0 +1,18 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { ResultsDatatable } from '@/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable'; + +export const edgeJobsModule = angular + .module('portainer.edge.react.components.edge-jobs', []) + .component( + 'edgeJobResultsDatatable', + r2a(withUIRouter(ResultsDatatable), [ + 'dataset', + 'onClearLogs', + 'onCollectLogs', + 'onDownloadLogs', + 'onRefresh', + ]) + ).name; diff --git a/app/edge/react/components/index.ts b/app/edge/react/components/index.ts index b3d0eceb4..61a2016e3 100644 --- a/app/edge/react/components/index.ts +++ b/app/edge/react/components/index.ts @@ -15,8 +15,10 @@ import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/Asso import { EnvironmentsDatatable } from '@/react/edge/edge-stacks/ItemView/EnvironmentsDatatable'; import { TemplateFieldset } from '@/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateFieldset'; +import { edgeJobsModule } from './edge-jobs'; + const ngModule = angular - .module('portainer.edge.react.components', []) + .module('portainer.edge.react.components', [edgeJobsModule]) .component( 'edgeStackEnvironmentsDatatable', r2a(withUIRouter(withReactQuery(EnvironmentsDatatable)), []) diff --git a/app/edge/views/edge-jobs/edgeJob/edgeJob.html b/app/edge/views/edge-jobs/edgeJob/edgeJob.html index 10b782cd8..152e59094 100644 --- a/app/edge/views/edge-jobs/edgeJob/edgeJob.html +++ b/app/edge/views/edge-jobs/edgeJob/edgeJob.html @@ -30,17 +30,13 @@ diff --git a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js index a65fa1275..da24e2e24 100644 --- a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js +++ b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js @@ -86,8 +86,14 @@ export class EdgeJobController { async collectLogsAsync(endpointId) { try { await this.EdgeJobService.collectLogs(this.edgeJob.Id, endpointId); - const result = _.find(this.results, (result) => result.EndpointId === endpointId); - result.LogsStatus = 2; + this.results = this.results.map((result) => + result.EndpointId === endpointId + ? { + ...result, + LogsStatus: 2, + } + : result + ); } catch (err) { this.Notifications.error('Failure', err, 'Unable to collect logs'); } @@ -99,8 +105,14 @@ export class EdgeJobController { async clearLogsAsync(endpointId) { try { await this.EdgeJobService.clearLogs(this.edgeJob.Id, endpointId); - const result = _.find(this.results, (result) => result.EndpointId === endpointId); - result.LogsStatus = 1; + this.results = this.results.map((result) => + result.EndpointId === endpointId + ? { + ...result, + LogsStatus: 1, + } + : result + ); } catch (err) { this.Notifications.error('Failure', err, 'Unable to clear logs'); } diff --git a/app/react/edge/edge-jobs/ItemView/.keep b/app/react/edge/edge-jobs/ItemView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable.tsx b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable.tsx new file mode 100644 index 000000000..68d520675 --- /dev/null +++ b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable.tsx @@ -0,0 +1,66 @@ +import { List } from 'lucide-react'; +import { useEffect } from 'react'; + +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { Datatable } from '@@/datatables'; +import { useTableState } from '@@/datatables/useTableState'; +import { withMeta } from '@@/datatables/extend-options/withMeta'; +import { useRepeater } from '@@/datatables/useRepeater'; + +import { LogsStatus } from '../../types'; + +import { DecoratedJobResult } from './types'; +import { columns } from './columns'; +import { createStore } from './datatable-store'; + +const tableKey = 'edge-job-results'; +const store = createStore(tableKey); + +export function ResultsDatatable({ + dataset, + onCollectLogs, + onClearLogs, + onDownloadLogs, + onRefresh, +}: { + dataset: Array; + + onCollectLogs(envId: EnvironmentId): void; + onDownloadLogs(envId: EnvironmentId): void; + onClearLogs(envId: EnvironmentId): void; + onRefresh(): void; +}) { + const anyCollecting = dataset.some( + (r) => r.LogsStatus === LogsStatus.Pending + ); + const tableState = useTableState(store, tableKey); + + const { setAutoRefreshRate } = tableState; + + useEffect(() => { + setAutoRefreshRate(anyCollecting ? 5 : 0); + }, [anyCollecting, setAutoRefreshRate]); + + useRepeater(tableState.autoRefreshRate, onRefresh); + return ( + + ); + + function handleCollectLogs(envId: EnvironmentId) { + onCollectLogs(envId); + } +} diff --git a/app/react/edge/edge-jobs/ItemView/ResultsDatatable/columns.tsx b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/columns.tsx new file mode 100644 index 000000000..7edfc3b0b --- /dev/null +++ b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/columns.tsx @@ -0,0 +1,60 @@ +import { CellContext, createColumnHelper } from '@tanstack/react-table'; + +import { Button } from '@@/buttons'; + +import { LogsStatus } from '../../types'; + +import { DecoratedJobResult, getTableMeta } from './types'; + +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.accessor('Endpoint.Name', { + header: 'Environment', + meta: { + className: 'w-1/2', + }, + }), + columnHelper.display({ + header: 'Actions', + cell: ActionsCell, + meta: { + className: 'w-1/2', + }, + }), +]; + +function ActionsCell({ + row: { original: item }, + table, +}: CellContext) { + const tableMeta = getTableMeta(table.options.meta); + + switch (item.LogsStatus) { + case LogsStatus.Pending: + return ( + <> + Logs marked for collection, please wait until the logs are available. + + ); + + case LogsStatus.Collected: + return ( + <> + + + + ); + case LogsStatus.Idle: + default: + return ( + + ); + } +} diff --git a/app/react/edge/edge-jobs/ItemView/ResultsDatatable/datatable-store.ts b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/datatable-store.ts new file mode 100644 index 000000000..d9ebb63a2 --- /dev/null +++ b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/datatable-store.ts @@ -0,0 +1,14 @@ +import { + refreshableSettings, + createPersistedStore, + BasicTableSettings, + RefreshableTableSettings, +} from '@@/datatables/types'; + +interface TableSettings extends BasicTableSettings, RefreshableTableSettings {} + +export function createStore(storageKey: string) { + return createPersistedStore(storageKey, undefined, (set) => ({ + ...refreshableSettings(set), + })); +} diff --git a/app/react/edge/edge-jobs/ItemView/ResultsDatatable/types.ts b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/types.ts new file mode 100644 index 000000000..911922ace --- /dev/null +++ b/app/react/edge/edge-jobs/ItemView/ResultsDatatable/types.ts @@ -0,0 +1,34 @@ +import { + Environment, + EnvironmentId, +} from '@/react/portainer/environments/types'; + +import { JobResult } from '../../types'; + +export interface DecoratedJobResult extends JobResult { + Endpoint: Environment; +} + +interface TableMeta { + table: 'edge-job-results'; + collectLogs(envId: EnvironmentId): void; + downloadLogs(envId: EnvironmentId): void; + clearLogs(envId: EnvironmentId): void; +} + +function isTableMeta(meta: unknown): meta is TableMeta { + return ( + !!meta && + typeof meta === 'object' && + 'table' in meta && + meta.table === 'edge-job-results' + ); +} + +export function getTableMeta(meta: unknown): TableMeta { + if (!isTableMeta(meta)) { + throw new Error('missing correct table meta'); + } + + return meta; +} diff --git a/app/react/edge/edge-jobs/types.ts b/app/react/edge/edge-jobs/types.ts index 1d1fdcc73..7327f5cfc 100644 --- a/app/react/edge/edge-jobs/types.ts +++ b/app/react/edge/edge-jobs/types.ts @@ -14,7 +14,7 @@ export interface EdgeJob { GroupLogsCollection: Record; } -enum LogsStatus { +export enum LogsStatus { Idle = 1, Pending = 2, Collected = 3, @@ -24,3 +24,9 @@ interface EndpointMeta { LogsStatus: LogsStatus; CollectLogs: boolean; } + +export interface JobResult { + Id: string; + EndpointId: EnvironmentId; + LogsStatus: LogsStatus; +}