refactor(docker/services): migrate service tasks to react [EE-4676] (#10328)

pull/10520/head
Chaim Lev-Ari 2023-10-23 13:52:49 +03:00 committed by GitHub
parent 70455320be
commit 1fa63f6ab7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 175 additions and 259 deletions

View File

@ -1,133 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for a task..."
ng-model-options="{ debounce: 300 }"
/>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table">
<thead>
<tr>
<th>
<table-column-header
col-title="'Status'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Status'"
is-sorted-desc="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Status')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Id'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Id'"
is-sorted-desc="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Id')"
></table-column-header>
</th>
<th>Actions</th>
<th ng-if="$ctrl.showSlotColumn">
<table-column-header
col-title="'Slot'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Slot'"
is-sorted-desc="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Slot')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Node'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'NodeId'"
is-sorted-desc="$ctrl.state.orderBy === 'NodeId' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('NodeId')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Last Update'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Updated'"
is-sorted-desc="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Updated')"
></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td
><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td
>
<td>
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced"
>{{ item.ServiceName }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a
>
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced"
>{{ item.ServiceName }}{{ item.Slot ? '.' + item.Slot : '' }}{{ '.' + item.Id }}</a
>
</td>
<td>
<task-table-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" task-id="item.Id" state="$ctrl.state"></task-table-quick-actions>
<container-quick-actions
ng-if="$ctrl.agentProxy && item.Container"
container-id="item.Container.Id"
node-name="item.Container.NodeName"
status="item.Status.State"
state="$ctrl.state"
></container-quick-actions>
</td>
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
<td>{{ item.Updated | getisodate }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-muted text-center">No task available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline vertical-center">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -1,16 +0,0 @@
angular.module('portainer.docker').component('tasksDatatable', {
templateUrl: './tasksDatatable.html',
controller: 'TasksDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
nodes: '<',
showSlotColumn: '<',
showLogsButton: '<',
agentProxy: '<',
},
});

View File

@ -1,49 +0,0 @@
angular.module('portainer.docker').controller('TasksDatatableController', [
'$scope',
'$controller',
'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
showQuickActionStats: true,
showQuickActionLogs: true,
showQuickActionExec: true,
showQuickActionInspect: true,
showQuickActionAttach: false,
});
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View File

@ -3,7 +3,6 @@ import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withControlledInput } from '@/react-tools/withControlledInput';
import { StackContainersDatatable } from '@/react/common/stacks/ItemView/StackContainersDatatable';
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
@ -40,15 +39,6 @@ const ngModule = angular
])
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
.component(
'containerQuickActions',
r2a(withUIRouter(withCurrentUser(ContainerQuickActions)), [
'containerId',
'nodeName',
'state',
'status',
])
)
.component('templateListDropdown', TemplateListDropdownAngular)
.component('templateListSort', TemplateListSortAngular)
.component(

View File

@ -2,22 +2,18 @@ import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { TasksDatatable } from '@/react/docker/services/ListView/ServicesDatatable/TasksDatatable';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions';
import { ServicesDatatable } from '@/react/docker/services/ListView/ServicesDatatable';
import { TasksDatatable } from '@/react/docker/services/ItemView/TasksDatatable';
export const servicesModule = angular
.module('portainer.docker.react.components.services', [])
.component(
'dockerServiceTasksDatatable',
r2a(withUIRouter(withCurrentUser(TasksDatatable)), ['dataset', 'search'])
)
.component(
'dockerTaskTableQuickActions',
r2a(withUIRouter(withCurrentUser(TaskTableQuickActions)), [
'state',
'taskId',
r2a(withUIRouter(withCurrentUser(TasksDatatable)), [
'serviceName',
'dataset',
'isSlotColumnVisible',
])
)
.component(

View File

@ -1,14 +1,6 @@
<div ng-if="tasks.length > 0 && nodes" id="service-tasks">
<tasks-datatable
title-text="Tasks"
title-icon="list"
dataset="tasks"
table-key="service-tasks"
order-by="Updated"
reverse-order="true"
nodes="nodes"
show-slot-column="service.Mode !== 'global'"
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
agent-proxy="applicationState.endpoint.mode.agentProxy"
></tasks-datatable>
</div>
<docker-service-tasks-datatable
ng-if="tasks.length > 0"
dataset="tasks"
is-slot-column-visible="service.Mode !== 'global'"
service-name="service.Name"
></docker-service-tasks-datatable>

View File

@ -253,6 +253,7 @@
<div id="service-labels" class="padding-top" ng-include="'app/docker/views/services/edit/includes/servicelabels.html'"></div>
<div id="service-configs" class="padding-top" ng-include="'app/docker/views/services/edit/includes/configs.html'"></div>
<div id="service-secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" class="padding-top" ng-include="'app/docker/views/services/edit/includes/secrets.html'"></div>
<div id="service-tasks" class="padding-top" ng-include="'app/docker/views/services/edit/includes/tasks.html'"></div>
</div>
</div>
<div id="service-tasks" class="padding-top" ng-include="'app/docker/views/services/edit/includes/tasks.html'"></div>

View File

@ -0,0 +1,37 @@
import { List } from 'lucide-react';
import { Datatable } from '@@/datatables';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { withMeta } from '@@/datatables/extend-options/withMeta';
import { useColumns } from './columns';
import { DecoratedTask } from './types';
const storageKey = 'docker-service-tasks';
const store = createPersistedStore(storageKey);
export function TasksDatatable({
dataset,
isSlotColumnVisible,
serviceName,
}: {
dataset: DecoratedTask[];
isSlotColumnVisible: boolean;
serviceName: string;
}) {
const tableState = useTableState(store, storageKey);
const columns = useColumns(isSlotColumnVisible);
return (
<Datatable
title="Tasks"
titleIcon={List}
settingsManager={tableState}
columns={columns}
dataset={dataset}
emptyContentLabel="No task available."
extendTableOptions={withMeta({ table: 'tasks', serviceName })}
/>
);
}

View File

@ -24,7 +24,7 @@ function Cell({
return null;
}
const state: QuickActionsState = {
showQuickActionAttach: true,
showQuickActionAttach: false,
showQuickActionExec: true,
showQuickActionInspect: true,
showQuickActionLogs: true,

View File

@ -0,0 +1,24 @@
import { useMemo } from 'react';
import _ from 'lodash';
import { actions } from './actions';
import { node } from './node';
import { slot } from './slot';
import { status } from './status';
import { task } from './task';
import { updated } from './updated';
export function useColumns(isSlotColumnsVisible = true) {
return useMemo(
() =>
_.compact([
status,
task,
actions,
isSlotColumnsVisible && slot,
node,
updated,
]),
[isSlotColumnsVisible]
);
}

View File

@ -0,0 +1,5 @@
import { columnHelper } from './helper';
export const slot = columnHelper.accessor((item) => item.Slot || '-', {
header: 'Slot',
});

View File

@ -0,0 +1,51 @@
import { CellContext } from '@tanstack/react-table';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
import { Link } from '@@/Link';
import { DecoratedTask } from '../types';
import { getTableMeta } from '../meta';
import { columnHelper } from './helper';
export const task = columnHelper.accessor('Id', {
header: 'Id',
cell: Cell,
});
function Cell({
getValue,
row: { original: item },
table: {
options: { meta },
},
}: CellContext<DecoratedTask, string>) {
const environmentQuery = useCurrentEnvironment();
if (!environmentQuery.data) {
return null;
}
const { serviceName } = getTableMeta(meta);
const value = getValue();
const isAgent = isAgentEnvironment(environmentQuery.data.Type);
const name = `${serviceName}${item.Slot ? `.${item.Slot}` : ''}.${value}`;
return isAgent && item.Container ? (
<Link
to="docker.containers.container"
params={{ id: item.Container.Id, nodeName: item.Container.NodeName }}
className="monospaced"
>
{name}
</Link>
) : (
<Link to="docker.tasks.task" params={{ id: value }} className="monospaced">
{name}
</Link>
);
}

View File

@ -0,0 +1,8 @@
import { isoDate } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const updated = columnHelper.accessor('Updated', {
header: 'Last Update',
cell: ({ getValue }) => isoDate(getValue()),
});

View File

@ -0,0 +1 @@
export { TasksDatatable } from './TasksDatatable';

View File

@ -0,0 +1,17 @@
type TableMeta = {
serviceName: string;
table: 'tasks';
};
export function getTableMeta(meta: unknown): TableMeta {
return isTableMeta(meta) ? meta : { table: 'tasks', serviceName: '' };
}
function isTableMeta(meta: unknown): meta is TableMeta {
return (
!!meta &&
typeof meta === 'object' &&
'table' in meta &&
meta.table === 'tasks'
);
}

View File

@ -19,6 +19,8 @@ import { getColumnVisibilityState } from '@@/datatables/ColumnVisibilityMenu';
import { mergeOptions } from '@@/datatables/extend-options/mergeOptions';
import { withGlobalFilter } from '@@/datatables/extend-options/withGlobalFilter';
import { DecoratedTask } from '../../ItemView/TasksDatatable/types';
import { useColumns } from './columns';
import { TasksDatatable } from './TasksDatatable';
import { TableActions } from './TableActions';
@ -72,7 +74,10 @@ export function ServicesDatatable({
<tr>
<td />
<td colSpan={Number.MAX_SAFE_INTEGER}>
<TasksDatatable dataset={item.Tasks} search={tableState.search} />
<TasksDatatable
dataset={item.Tasks as Array<DecoratedTask>}
search={tableState.search}
/>
</td>
</tr>
)}

View File

@ -1,7 +1,15 @@
import { DecoratedTask } from '@/react/docker/services/ItemView/TasksDatatable/types';
import { status } from '@/react/docker/services/ItemView/TasksDatatable/columns/status';
import { actions } from '@/react/docker/services/ItemView/TasksDatatable/columns/actions';
import { slot } from '@/react/docker/services/ItemView/TasksDatatable/columns/slot';
import { node } from '@/react/docker/services/ItemView/TasksDatatable/columns/node';
import { updated } from '@/react/docker/services/ItemView/TasksDatatable/columns/updated';
import { NestedDatatable } from '@@/datatables/NestedDatatable';
import { columns } from './columns';
import { DecoratedTask } from './types';
import { task } from './task-column';
const columns = [status, task, actions, slot, node, updated];
export function TasksDatatable({
dataset,

View File

@ -1,19 +0,0 @@
import { isoDate } from '@/portainer/filters/filters';
import { actions } from './actions';
import { columnHelper } from './helper';
import { node } from './node';
import { status } from './status';
import { task } from './task';
export const columns = [
status,
task,
actions,
columnHelper.accessor((item) => item.Slot || '-', { header: 'Slot' }),
node,
columnHelper.accessor('Updated', {
header: 'Last Update',
cell: ({ getValue }) => isoDate(getValue()),
}),
];

View File

@ -2,13 +2,11 @@ import { CellContext } from '@tanstack/react-table';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
import { DecoratedTask } from '@/react/docker/services/ItemView/TasksDatatable/types';
import { columnHelper } from '@/react/docker/services/ItemView/TasksDatatable/columns/helper';
import { Link } from '@@/Link';
import { DecoratedTask } from '../types';
import { columnHelper } from './helper';
export const task = columnHelper.accessor('Id', {
header: 'Task',
cell: Cell,