mirror of https://github.com/portainer/portainer
close [EE-4337]refactor/EE-4337/service-task-datatable
parent
0ee6c5c6e9
commit
8ab739adfd
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
@ -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 || '';
|
||||
}
|
||||
}
|
@ -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;
|
@ -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,6 @@
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
export const queryKeys = {
|
||||
base: (environmentId: EnvironmentId) =>
|
||||
[environmentId, 'docker', 'proxy'] as const,
|
||||
};
|
@ -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