mirror of https://github.com/portainer/portainer
refactor(docker/services): convert service tasks table to react [EE-4674] (#10188)
parent
c47a804c97
commit
c3d266931f
|
@ -1,109 +0,0 @@
|
||||||
<div class="inner-datatable">
|
|
||||||
<table class="table-condensed table-hover nowrap-cells table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" class="w-[10%]">
|
|
||||||
<div class="flex">
|
|
||||||
<table-column-header
|
|
||||||
col-title="'Status'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'Status.State'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('Status.State')"
|
|
||||||
></table-column-header>
|
|
||||||
<span class="space-left">
|
|
||||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled"
|
|
||||||
>Filter
|
|
||||||
<pr-icon icon="'filter'"></pr-icon>
|
|
||||||
</span>
|
|
||||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled"
|
|
||||||
>Filter
|
|
||||||
<pr-icon icon="'check'"></pr-icon>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="dropdown-menu" uib-dropdown-menu>
|
|
||||||
<div class="tableMenu">
|
|
||||||
<div class="menuHeader"> Filter by state </div>
|
|
||||||
<div class="menuContent">
|
|
||||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
|
||||||
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
|
||||||
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th style="width: 22%">Task</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
<th>
|
|
||||||
<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
|
|
||||||
ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))"
|
|
||||||
>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ 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.Id
|
|
||||||
}}</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<container-quick-actions
|
|
||||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
|
||||||
container-id="item.ContainerId"
|
|
||||||
task-id="item.Id"
|
|
||||||
status="item.Status.State"
|
|
||||||
state="$ctrl.state"
|
|
||||||
></container-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>{{ item.Slot ? item.Slot : '-' }}</td>
|
|
||||||
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
|
||||||
<td>{{ item.Updated | getisodate }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
|
||||||
<td colspan="5" class="text-muted text-center">No task matching filter.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
|
@ -1,15 +0,0 @@
|
||||||
angular.module('portainer.docker').component('serviceTasksDatatable', {
|
|
||||||
templateUrl: './serviceTasksDatatable.html',
|
|
||||||
controller: 'ServiceTasksDatatableController',
|
|
||||||
bindings: {
|
|
||||||
dataset: '<',
|
|
||||||
serviceId: '<',
|
|
||||||
tableKey: '@',
|
|
||||||
orderBy: '@',
|
|
||||||
reverseOrder: '<',
|
|
||||||
nodes: '<',
|
|
||||||
agentProxy: '<',
|
|
||||||
textFilter: '=',
|
|
||||||
showTaskLogsButton: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,94 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('ServiceTasksDatatableController', [
|
|
||||||
'$scope',
|
|
||||||
'$controller',
|
|
||||||
'DatatableService',
|
|
||||||
function ($scope, $controller, DatatableService) {
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
|
||||||
|
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.state = Object.assign(this.state, {
|
|
||||||
showQuickActionStats: true,
|
|
||||||
showQuickActionLogs: true,
|
|
||||||
showQuickActionConsole: true,
|
|
||||||
showQuickActionInspect: true,
|
|
||||||
showQuickActionExec: true,
|
|
||||||
showQuickActionAttach: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.filters = {
|
|
||||||
state: {
|
|
||||||
open: false,
|
|
||||||
enabled: false,
|
|
||||||
values: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyFilters = function (item) {
|
|
||||||
var filters = ctrl.filters;
|
|
||||||
for (var i = 0; i < filters.state.values.length; i++) {
|
|
||||||
var filter = filters.state.values[i];
|
|
||||||
if (item.Status.State === filter.label && filter.display) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onStateFilterChange = function () {
|
|
||||||
var filters = this.filters.state.values;
|
|
||||||
var filtered = false;
|
|
||||||
for (var i = 0; i < filters.length; i++) {
|
|
||||||
var filter = filters[i];
|
|
||||||
if (!filter.display) {
|
|
||||||
filtered = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.filters.state.enabled = filtered;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.prepareTableFromDataset = function () {
|
|
||||||
var availableStateFilters = [];
|
|
||||||
for (var i = 0; i < this.dataset.length; i++) {
|
|
||||||
var item = this.dataset[i];
|
|
||||||
availableStateFilters.push({ label: item.Status.State, display: true });
|
|
||||||
}
|
|
||||||
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -210,16 +210,7 @@
|
||||||
<tr dir-paginate-end ng-show="item.Expanded">
|
<tr dir-paginate-end ng-show="item.Expanded">
|
||||||
<td></td>
|
<td></td>
|
||||||
<td colspan="8">
|
<td colspan="8">
|
||||||
<service-tasks-datatable
|
<docker-service-tasks-datatable dataset="item.Tasks" search="$ctrl.state.textFilter"></docker-service-tasks-datatable>
|
||||||
dataset="item.Tasks"
|
|
||||||
service-id="item.Id"
|
|
||||||
table-key="service-tasks"
|
|
||||||
order-by="Status.State"
|
|
||||||
nodes="$ctrl.nodes"
|
|
||||||
agent-proxy="$ctrl.agentProxy"
|
|
||||||
show-task-logs-button="$ctrl.showTaskLogsButton"
|
|
||||||
text-filter="$ctrl.state.textFilter"
|
|
||||||
></service-tasks-datatable>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
|
|
@ -89,13 +89,7 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<container-quick-actions
|
<task-table-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" task-id="item.Id" state="$ctrl.state"></task-table-quick-actions>
|
||||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
|
||||||
container-id="item.ContainerId"
|
|
||||||
task-id="item.Id"
|
|
||||||
status="item.Status.State"
|
|
||||||
state="$ctrl.state"
|
|
||||||
></container-quick-actions>
|
|
||||||
<container-quick-actions
|
<container-quick-actions
|
||||||
ng-if="$ctrl.agentProxy && item.Container"
|
ng-if="$ctrl.agentProxy && item.Container"
|
||||||
container-id="item.Container.Id"
|
container-id="item.Container.Id"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { joinCommand, trimSHA } from './utils';
|
import { joinCommand, taskStatusBadge, trimSHA } from './utils';
|
||||||
|
|
||||||
function includeString(text, values) {
|
function includeString(text, values) {
|
||||||
return values.some(function (val) {
|
return values.some(function (val) {
|
||||||
|
@ -49,22 +49,7 @@ angular
|
||||||
})
|
})
|
||||||
.filter('taskstatusbadge', function () {
|
.filter('taskstatusbadge', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text) {
|
return taskStatusBadge;
|
||||||
var status = _.toLower(text);
|
|
||||||
var labelStyle = 'default';
|
|
||||||
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) {
|
|
||||||
labelStyle = 'info';
|
|
||||||
} else if (includeString(status, ['pending'])) {
|
|
||||||
labelStyle = 'warning';
|
|
||||||
} else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) {
|
|
||||||
labelStyle = 'danger';
|
|
||||||
} else if (includeString(status, ['complete'])) {
|
|
||||||
labelStyle = 'primary';
|
|
||||||
} else if (includeString(status, ['running'])) {
|
|
||||||
labelStyle = 'success';
|
|
||||||
}
|
|
||||||
return labelStyle;
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.filter('taskhaslogs', function () {
|
.filter('taskhaslogs', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { TaskState } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
export function trimSHA(imageName: string) {
|
export function trimSHA(imageName: string) {
|
||||||
if (!imageName) {
|
if (!imageName) {
|
||||||
|
@ -17,3 +18,38 @@ export function joinCommand(command: null | Array<string> = []) {
|
||||||
|
|
||||||
return command.join(' ');
|
return command.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function taskStatusBadge(text?: TaskState) {
|
||||||
|
const status = _.toLower(text);
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'new',
|
||||||
|
'allocated',
|
||||||
|
'assigned',
|
||||||
|
'accepted',
|
||||||
|
'preparing',
|
||||||
|
'ready',
|
||||||
|
'starting',
|
||||||
|
'remove',
|
||||||
|
].includes(status)
|
||||||
|
) {
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['pending'].includes(status)) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['shutdown', 'failed', 'rejected', 'orphaned'].includes(status)) {
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['complete'].includes(status)) {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['running'].includes(status)) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
export function TaskViewModel(data) {
|
|
||||||
this.Id = data.ID;
|
|
||||||
this.Created = data.CreatedAt;
|
|
||||||
this.Updated = data.UpdatedAt;
|
|
||||||
this.Slot = data.Slot;
|
|
||||||
this.Spec = data.Spec;
|
|
||||||
this.Status = data.Status;
|
|
||||||
this.DesiredState = data.DesiredState;
|
|
||||||
this.ServiceId = data.ServiceID;
|
|
||||||
this.NodeId = data.NodeID;
|
|
||||||
if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) {
|
|
||||||
this.ContainerId = data.Status.ContainerStatus.ContainerID;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
|
export class TaskViewModel {
|
||||||
|
Id: string;
|
||||||
|
|
||||||
|
Created: string;
|
||||||
|
|
||||||
|
Updated: string;
|
||||||
|
|
||||||
|
Slot: number;
|
||||||
|
|
||||||
|
Spec?: TaskSpec;
|
||||||
|
|
||||||
|
Status: Task['Status'];
|
||||||
|
|
||||||
|
DesiredState: TaskState;
|
||||||
|
|
||||||
|
ServiceId: string;
|
||||||
|
|
||||||
|
NodeId: string;
|
||||||
|
|
||||||
|
ContainerId: string = '';
|
||||||
|
|
||||||
|
constructor(data: Task) {
|
||||||
|
this.Id = data.ID || '';
|
||||||
|
this.Created = data.CreatedAt || '';
|
||||||
|
this.Updated = data.UpdatedAt || '';
|
||||||
|
this.Slot = data.Slot || 0;
|
||||||
|
this.Spec = data.Spec;
|
||||||
|
this.Status = data.Status;
|
||||||
|
this.DesiredState = data.DesiredState || 'pending';
|
||||||
|
this.ServiceId = data.ServiceID || '';
|
||||||
|
this.NodeId = data.NodeID || '';
|
||||||
|
this.ContainerId = data.Status?.ContainerStatus?.ContainerID || '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,9 +25,13 @@ import { ScaleServiceButton } from '@/react/docker/services/ListView/ServicesDat
|
||||||
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
|
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
|
||||||
|
|
||||||
import { containersModule } from './containers';
|
import { containersModule } from './containers';
|
||||||
|
import { servicesModule } from './services';
|
||||||
|
|
||||||
const ngModule = angular
|
const ngModule = angular
|
||||||
.module('portainer.docker.react.components', [containersModule])
|
.module('portainer.docker.react.components', [
|
||||||
|
containersModule,
|
||||||
|
servicesModule,
|
||||||
|
])
|
||||||
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
||||||
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
||||||
.component(
|
.component(
|
||||||
|
@ -37,7 +41,6 @@ const ngModule = angular
|
||||||
'nodeName',
|
'nodeName',
|
||||||
'state',
|
'state',
|
||||||
'status',
|
'status',
|
||||||
'taskId',
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component('templateListDropdown', TemplateListDropdownAngular)
|
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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',
|
||||||
|
])
|
||||||
|
).name;
|
|
@ -23,6 +23,11 @@ interface Props<D extends DefaultType> {
|
||||||
initialTableState?: Partial<TableState>;
|
initialTableState?: Partial<TableState>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
initialSortBy?: BasicTableSettings['sortBy'];
|
initialSortBy?: BasicTableSettings['sortBy'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyword to filter by
|
||||||
|
*/
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NestedDatatable<D extends DefaultType>({
|
export function NestedDatatable<D extends DefaultType>({
|
||||||
|
@ -33,6 +38,7 @@ export function NestedDatatable<D extends DefaultType>({
|
||||||
initialTableState = {},
|
initialTableState = {},
|
||||||
isLoading,
|
isLoading,
|
||||||
initialSortBy,
|
initialSortBy,
|
||||||
|
search,
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
const tableInstance = useReactTable<D>({
|
const tableInstance = useReactTable<D>({
|
||||||
columns,
|
columns,
|
||||||
|
@ -45,6 +51,9 @@ export function NestedDatatable<D extends DefaultType>({
|
||||||
enableColumnFilter: false,
|
enableColumnFilter: false,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
},
|
},
|
||||||
|
state: {
|
||||||
|
globalFilter: search,
|
||||||
|
},
|
||||||
getRowId,
|
getRowId,
|
||||||
autoResetExpanded: false,
|
autoResetExpanded: false,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
@ -55,7 +64,7 @@ export function NestedDatatable<D extends DefaultType>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NestedTable>
|
<NestedTable>
|
||||||
<Table.Container>
|
<Table.Container noWidget>
|
||||||
<DatatableContent<D>
|
<DatatableContent<D>
|
||||||
tableInstance={tableInstance}
|
tableInstance={tableInstance}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Link } from '@@/Link';
|
||||||
|
|
||||||
import styles from './ContainerQuickActions.module.css';
|
import styles from './ContainerQuickActions.module.css';
|
||||||
|
|
||||||
interface QuickActionsState {
|
export interface QuickActionsState {
|
||||||
showQuickActionAttach: boolean;
|
showQuickActionAttach: boolean;
|
||||||
showQuickActionExec: boolean;
|
showQuickActionExec: boolean;
|
||||||
showQuickActionInspect: boolean;
|
showQuickActionInspect: boolean;
|
||||||
|
@ -17,31 +17,25 @@ interface QuickActionsState {
|
||||||
showQuickActionStats: boolean;
|
showQuickActionStats: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
|
||||||
taskId?: string;
|
|
||||||
containerId?: string;
|
|
||||||
nodeName: string;
|
|
||||||
state: QuickActionsState;
|
|
||||||
status: ContainerStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContainerQuickActions({
|
export function ContainerQuickActions({
|
||||||
taskId,
|
status,
|
||||||
containerId,
|
containerId,
|
||||||
nodeName,
|
nodeName,
|
||||||
state,
|
state,
|
||||||
status,
|
}: {
|
||||||
}: Props) {
|
containerId: string;
|
||||||
if (taskId) {
|
nodeName: string;
|
||||||
return <TaskQuickActions taskId={taskId} state={state} />;
|
status: ContainerStatus;
|
||||||
}
|
state: QuickActionsState;
|
||||||
|
}) {
|
||||||
const isActive = [
|
const isActive =
|
||||||
ContainerStatus.Starting,
|
!!status &&
|
||||||
ContainerStatus.Running,
|
[
|
||||||
ContainerStatus.Healthy,
|
ContainerStatus.Starting,
|
||||||
ContainerStatus.Unhealthy,
|
ContainerStatus.Running,
|
||||||
].includes(status);
|
ContainerStatus.Healthy,
|
||||||
|
ContainerStatus.Unhealthy,
|
||||||
|
].includes(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('space-x-1', styles.root)}>
|
<div className={clsx('space-x-1', styles.root)}>
|
||||||
|
@ -107,34 +101,3 @@ export function ContainerQuickActions({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskProps {
|
|
||||||
taskId: string;
|
|
||||||
state: QuickActionsState;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TaskQuickActions({ taskId, state }: TaskProps) {
|
|
||||||
return (
|
|
||||||
<div className={clsx('space-x-1', styles.root)}>
|
|
||||||
{state.showQuickActionLogs && (
|
|
||||||
<Authorized authorizations="DockerTaskLogs">
|
|
||||||
<Link
|
|
||||||
to="docker.tasks.task.logs"
|
|
||||||
params={{ id: taskId }}
|
|
||||||
title="Logs"
|
|
||||||
>
|
|
||||||
<Icon icon={FileText} className="space-right" />
|
|
||||||
</Link>
|
|
||||||
</Authorized>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{state.showQuickActionInspect && (
|
|
||||||
<Authorized authorizations="DockerTaskInspect">
|
|
||||||
<Link to="docker.tasks.task" params={{ id: taskId }} title="Inspect">
|
|
||||||
<Icon icon={Info} className="space-right" />
|
|
||||||
</Link>
|
|
||||||
</Authorized>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildUrl as buildProxyUrl } from '../build-url';
|
||||||
|
|
||||||
|
export function buildUrl(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
action?: string,
|
||||||
|
subAction = ''
|
||||||
|
) {
|
||||||
|
return buildProxyUrl(
|
||||||
|
environmentId,
|
||||||
|
'nodes',
|
||||||
|
subAction ? `${action}/${subAction}` : action
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { queryKeys as proxyQueryKeys } from '../query-keys';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
base: (environmentId: EnvironmentId) =>
|
||||||
|
[...proxyQueryKeys.base(environmentId), 'nodes'] as const,
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Node } from 'docker-types/generated/1.41';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useNodes(environmentId: EnvironmentId) {
|
||||||
|
return useQuery(queryKeys.base(environmentId), () => getNodes(environmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNodes(environmentId: EnvironmentId) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<Array<Node>>(buildUrl(environmentId));
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error, 'Unable to retrieve nodes');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
||||||
|
|
||||||
|
import { columns } from './columns';
|
||||||
|
import { DecoratedTask } from './types';
|
||||||
|
|
||||||
|
export function TasksDatatable({
|
||||||
|
dataset,
|
||||||
|
search,
|
||||||
|
}: {
|
||||||
|
dataset: DecoratedTask[];
|
||||||
|
search?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NestedDatatable
|
||||||
|
columns={columns}
|
||||||
|
dataset={dataset}
|
||||||
|
search={search}
|
||||||
|
emptyContentLabel="No task matching filter."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
|
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||||
|
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
|
||||||
|
import { QuickActionsState } from '@/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions';
|
||||||
|
import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions';
|
||||||
|
|
||||||
|
import { DecoratedTask } from '../types';
|
||||||
|
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
|
export const actions = columnHelper.display({
|
||||||
|
header: 'Actions',
|
||||||
|
cell: Cell,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Cell({
|
||||||
|
row: { original: item },
|
||||||
|
}: CellContext<DecoratedTask, unknown>) {
|
||||||
|
const environmentQuery = useCurrentEnvironment();
|
||||||
|
|
||||||
|
if (!environmentQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const state: QuickActionsState = {
|
||||||
|
showQuickActionAttach: true,
|
||||||
|
showQuickActionExec: true,
|
||||||
|
showQuickActionInspect: true,
|
||||||
|
showQuickActionLogs: true,
|
||||||
|
showQuickActionStats: true,
|
||||||
|
};
|
||||||
|
const isAgent = isAgentEnvironment(environmentQuery.data.Type);
|
||||||
|
|
||||||
|
return isAgent && item.Container ? (
|
||||||
|
<ContainerQuickActions
|
||||||
|
containerId={item.Container.Id}
|
||||||
|
nodeName={item.Container.NodeName}
|
||||||
|
status={item.Container.Status}
|
||||||
|
state={state}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TaskTableQuickActions taskId={item.Id} />
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { DecoratedTask } from '../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<DecoratedTask>();
|
|
@ -0,0 +1,19 @@
|
||||||
|
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()),
|
||||||
|
}),
|
||||||
|
];
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Node } from 'docker-types/generated/1.41';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { useNodes } from '@/react/docker/proxy/queries/nodes/useNodes';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { DecoratedTask } from '../types';
|
||||||
|
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
|
export const node = columnHelper.accessor('NodeId', {
|
||||||
|
header: 'Node',
|
||||||
|
cell: Cell,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Cell({ getValue }: CellContext<DecoratedTask, string>) {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
|
const nodesQuery = useNodes(environmentId);
|
||||||
|
|
||||||
|
const nodes = nodesQuery.data || [];
|
||||||
|
return getNodeName(getValue(), nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeName(nodeId: string, nodes: Array<Node>) {
|
||||||
|
const node = nodes.find((node) => node.ID === nodeId);
|
||||||
|
if (node?.Description?.Hostname) {
|
||||||
|
return node.Description.Hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { taskStatusBadge } from '@/docker/filters/utils';
|
||||||
|
|
||||||
|
import { multiple } from '@@/datatables/filter-types';
|
||||||
|
import { filterHOC } from '@@/datatables/Filter';
|
||||||
|
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
|
export const status = columnHelper.accessor((item) => item.Status?.State, {
|
||||||
|
header: 'Status',
|
||||||
|
enableColumnFilter: true,
|
||||||
|
filterFn: multiple,
|
||||||
|
meta: {
|
||||||
|
filter: filterHOC('Filter by state'),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
cell({ getValue }) {
|
||||||
|
const value = getValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={clsx('label', `label-${taskStatusBadge(value)}`)}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
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 { columnHelper } from './helper';
|
||||||
|
|
||||||
|
export const task = columnHelper.accessor('Id', {
|
||||||
|
header: 'Task',
|
||||||
|
cell: Cell,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Cell({
|
||||||
|
getValue,
|
||||||
|
row: { original: item },
|
||||||
|
}: CellContext<DecoratedTask, string>) {
|
||||||
|
const environmentQuery = useCurrentEnvironment();
|
||||||
|
|
||||||
|
if (!environmentQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = getValue();
|
||||||
|
const isAgent = isAgentEnvironment(environmentQuery.data.Type);
|
||||||
|
|
||||||
|
return isAgent && item.Container ? (
|
||||||
|
<Link
|
||||||
|
to="docker.containers.container"
|
||||||
|
params={{ id: item.Container.Id, nodeName: item.Container.NodeName }}
|
||||||
|
className="monospaced"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to="docker.tasks.task"
|
||||||
|
params={{ id: item.Id }}
|
||||||
|
className="monospaced"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { TasksDatatable } from './TasksDatatable';
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { TaskViewModel } from '@/docker/models/task';
|
||||||
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
|
||||||
|
export type DecoratedTask = TaskViewModel & {
|
||||||
|
Container?: DockerContainer;
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { FileText, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showQuickActionInspect: boolean;
|
||||||
|
showQuickActionLogs: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskTableQuickActions({
|
||||||
|
taskId,
|
||||||
|
state = {
|
||||||
|
showQuickActionInspect: true,
|
||||||
|
showQuickActionLogs: true,
|
||||||
|
},
|
||||||
|
}: {
|
||||||
|
taskId: string;
|
||||||
|
state?: State;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex space-x-1">
|
||||||
|
{state.showQuickActionLogs && (
|
||||||
|
<Authorized authorizations="DockerTaskLogs">
|
||||||
|
<Link
|
||||||
|
to="docker.tasks.task.logs"
|
||||||
|
params={{ id: taskId }}
|
||||||
|
title="Logs"
|
||||||
|
>
|
||||||
|
<Icon icon={FileText} className="space-right" />
|
||||||
|
</Link>
|
||||||
|
</Authorized>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.showQuickActionInspect && (
|
||||||
|
<Authorized authorizations="DockerTaskInspect">
|
||||||
|
<Link to="docker.tasks.task" params={{ id: taskId }} title="Inspect">
|
||||||
|
<Icon icon={Info} className="space-right" />
|
||||||
|
</Link>
|
||||||
|
</Authorized>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue