refactor(kube/namespaces): migrate table to react [EE-4694] (#10988)

pull/11016/head
Chaim Lev-Ari 8 months ago committed by GitHub
parent da615afc92
commit 26bb028ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,208 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<!-- toolbar header actions and settings -->
<div class="toolBar !flex-col gap-1">
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="'layers'"></pr-icon>
</div>
Namespaces
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sNamespace-namespaceSearchInput"
aria-label="Search input"
/>
</div>
<div class="actionBar !mr-0 !gap-3" ng-if="$ctrl.isAdmin">
<button
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sNamespace-removeNamespaceButton"
>
<pr-icon icon="'trash-2'"></pr-icon>Remove
</button>
<button type="button" class="btn btn-sm btn-secondary !ml-0" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceWithFormButton">
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
</button>
<button
type="button"
class="btn btn-sm btn-primary !ml-0"
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.resourcePools' })"
data-cy="k8sNamespace-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
</button>
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle>
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="flex w-full flex-row" ng-if="!$ctrl.settings.showSystem && $ctrl.isAdmin">
<span class="small text-muted vertical-center mt-1">
<pr-icon icon="'info'" mode="'primary'" class="vertical-center"></pr-icon>
<div> System resources are hidden, this can be changed in the table settings. </div>
</span>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table">
<thead>
<tr>
<th class="dropdown">
<div class="vertical-center flex min-w-max flex-nowrap">
<span ng-if="$ctrl.isAdmin" class="md-checkbox vertical-center">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Namespace.Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Namespace.Name')"
></table-column-header>
</div>
</th>
<th>
<table-column-header
col-title="'Status'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Namespace.Status'"
is-sorted-desc="$ctrl.state.orderBy === 'Namespace.Status' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Namespace.Status')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Quota'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Quota'"
is-sorted-desc="$ctrl.state.orderBy === 'Quota' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Quota')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Created'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Namespace.CreationDate'"
is-sorted-desc="$ctrl.state.orderBy === 'Namespace.CreationDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Namespace.CreationDate')"
></table-column-header>
</th>
<th ng-if="$ctrl.isAdmin">
<table-column-header col-title="'Actions'" can-sort="false"></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span ng-if="$ctrl.isAdmin" class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace.Name })">{{ item.Namespace.Name }}</a>
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</td>
<td>
<span class="label label-{{ $ctrl.namespaceStatusColor(item.Namespace.Status) }}">{{ item.Namespace.Status }}</span>
</td>
<td>
<span ng-if="item.Quota" class="label label-warning">Enabled</span>
<span ng-if="!item.Quota">-</span>
</td>
<td>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td ng-if="$ctrl.isAdmin">
<a class="vertical-center" ng-if="$ctrl.canManageAccess(item)" ui-sref="kubernetes.resourcePools.resourcePool.access({id: item.Namespace.Name})">
<pr-icon icon="'users'"></pr-icon>
Manage access
</a>
<span ng-if="!$ctrl.canManageAccess(item)">-</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-muted text-center">No namespace available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer pl-5" ng-if="$ctrl.dataset">
<div class="infoBar !ml-0" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<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>

@ -1,15 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', {
templateUrl: './resourcePoolsDatatable.html',
controller: 'KubernetesResourcePoolsDatatableController',
bindings: {
restrictDefaultNamespace: '<',
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
refreshCallback: '<',
},
});

@ -1,93 +0,0 @@
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [
'$scope',
'$controller',
'Authentication',
'DatatableService',
function ($scope, $controller, Authentication, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsShowSystemChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.canManageAccess = function (item) {
if (!this.restrictDefaultNamespace) {
return !KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name) && !this.isSystemNamespace(item);
} else {
return !this.isSystemNamespace(item);
}
};
this.disableRemove = function (item) {
return this.isSystemNamespace(item) || KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
};
this.namespaceStatusColor = function (status) {
switch (status.toLowerCase()) {
case 'active':
return 'success';
case 'terminating':
return 'danger';
default:
return 'primary';
}
};
/**
* Do not allow system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.disableRemove(item);
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
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,7 +1,8 @@
import _ from 'lodash-es';
import { KUBERNETES_DEFAULT_NAMESPACE, KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models';
import { KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models';
import { isSystem } from 'Kubernetes/store/namespace';
import { isDefaultNamespace } from '@/react/kubernetes/namespaces/isDefaultNamespace';
export default class KubernetesNamespaceHelper {
/**
@ -19,7 +20,7 @@ export default class KubernetesNamespaceHelper {
* @returns Boolean
*/
static isDefaultNamespace(namespace) {
return namespace === KUBERNETES_DEFAULT_NAMESPACE;
return isDefaultNamespace(namespace);
}
/**

@ -12,4 +12,3 @@ export class KubernetesNamespace {
}
export const KUBERNETES_DEFAULT_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer'];
export const KUBERNETES_DEFAULT_NAMESPACE = 'default';

@ -63,11 +63,13 @@ import { HelmInsightsBox } from '@/react/kubernetes/applications/ListView/Applic
import { applicationsModule } from './applications';
import { volumesModule } from './volumes';
import { namespacesModule } from './namespaces';
export const ngModule = angular
.module('portainer.kubernetes.react.components', [
applicationsModule,
volumesModule,
namespacesModule,
])
.component(
'ingressClassDatatable',

@ -0,0 +1,18 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { NamespacesDatatable } from '@/react/kubernetes/namespaces/ListView/NamespacesDatatable';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
export const namespacesModule = angular
.module('portainer.kubernetes.react.components.namespaces', [])
.component(
'kubernetesNamespacesDatatable',
r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [
'dataset',
'onRemove',
'onRefresh',
])
).name;

@ -3,18 +3,5 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-sm-12">
<kubernetes-resource-pools-datatable
restrict-default-namespace="ctrl.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace"
dataset="ctrl.resourcePools"
table-key="kubernetes.resourcePools"
order-by="Name"
remove-action="ctrl.removeAction"
refresh-callback="ctrl.getResourcePools"
endpoint="ctrl.endpoint"
data-cy="k8sNamespace-namespaceTable"
></kubernetes-resource-pools-datatable>
</div>
</div>
<kubernetes-namespaces-datatable dataset="ctrl.resourcePools" on-remove="(ctrl.removeAction)" on-refresh="(ctrl.getResourcePools)"></kubernetes-namespaces-datatable>
</div>

@ -0,0 +1,67 @@
import { Meta, StoryObj } from '@storybook/react';
import { Check } from 'lucide-react';
import { StatusBadge } from './StatusBadge';
const meta: Meta<typeof StatusBadge> = {
title: 'Components/StatusBadge',
component: StatusBadge,
};
export default meta;
type Story = StoryObj<typeof StatusBadge>;
export const Default: Story = {
args: {
children: 'Default',
},
};
export const WithIcon: Story = {
args: {
icon: Check,
children: 'With Icon',
},
};
export const Success: Story = {
args: {
color: 'success',
children: 'Success',
},
};
export const Warning: Story = {
args: {
color: 'warning',
children: 'Warning',
},
};
export const Danger: Story = {
args: {
color: 'danger',
children: 'Danger',
},
};
export const WithAriaAttributes: Story = {
args: {
'aria-label': 'Badge with Aria Attributes',
children: 'With Aria Attributes',
},
};
export const WithChildren: Story = {
args: {
children: (
<>
<span role="img" aria-label="Star">
</span>
With Children
</>
),
},
};

@ -12,20 +12,21 @@ export function StatusBadge({
}: PropsWithChildren<
{
className?: string;
color?: 'success' | 'danger' | 'warning' | 'default';
color?: 'success' | 'danger' | 'warning' | 'info' | 'default';
icon?: IconProps['icon'];
} & AriaAttributes
>) {
return (
<span
className={clsx(
'flex items-center gap-1 rounded',
'inline-flex items-center gap-1 rounded',
'w-fit px-1.5 py-0.5',
'text-sm font-medium text-white',
{
' bg-success-7 th-dark:bg-success-9': color === 'success',
' bg-warning-7 th-dark:bg-warning-9': color === 'warning',
' bg-error-7 th-dark:bg-error-9': color === 'danger',
'bg-success-7 th-dark:bg-success-9': color === 'success',
'bg-warning-7 th-dark:bg-warning-9': color === 'warning',
'bg-error-7 th-dark:bg-error-9': color === 'danger',
'bg-blue-9': color === 'info',
},
className
)}

@ -5,18 +5,30 @@ import { confirmDelete } from '@@/modals/confirm';
import { Button } from './Button';
type ConfirmOrClick =
| {
confirmMessage: ReactNode;
onConfirmed(): Promise<void> | void;
onClick?: never;
}
| {
confirmMessage?: never;
onConfirmed?: never;
/** if onClick is set, will skip confirmation (confirmation should be done on the parent) */
onClick(): void;
};
export function DeleteButton({
disabled,
confirmMessage,
onConfirmed,
size,
children,
}: PropsWithChildren<{
size?: ComponentProps<typeof Button>['size'];
disabled?: boolean;
confirmMessage: ReactNode;
onConfirmed(): Promise<void> | void;
}>) {
...props
}: PropsWithChildren<
ConfirmOrClick & {
size?: ComponentProps<typeof Button>['size'];
disabled?: boolean;
}
>) {
return (
<Button
size={size}
@ -31,6 +43,11 @@ export function DeleteButton({
);
async function handleClick() {
const { confirmMessage, onConfirmed, onClick } = props;
if (onClick) {
return onClick();
}
if (!(await confirmDelete(confirmMessage))) {
return undefined;
}

@ -0,0 +1,92 @@
import { Layers } from 'lucide-react';
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
import { refreshableSettings } from '@@/datatables/types';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import { useTableStateWithStorage } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { useRepeater } from '@@/datatables/useRepeater';
import { AddButton } from '@@/buttons';
import { systemResourcesSettings } from '../../datatables/SystemResourcesSettings';
import { CreateFromManifestButton } from '../../components/CreateFromManifestButton';
import {
DefaultDatatableSettings,
TableSettings,
} from '../../datatables/DefaultDatatableSettings';
import { SystemResourceDescription } from '../../datatables/SystemResourceDescription';
import { isDefaultNamespace } from '../isDefaultNamespace';
import { NamespaceViewModel } from './types';
import { useColumns } from './columns/useColumns';
export function NamespacesDatatable({
dataset,
onRemove,
onRefresh,
}: {
dataset: Array<NamespaceViewModel>;
onRemove(items: Array<NamespaceViewModel>): void;
onRefresh(): void;
}) {
const tableState = useTableStateWithStorage<TableSettings>(
'kube-namespaces',
'Name',
(set) => ({
...systemResourcesSettings(set),
...refreshableSettings(set),
})
);
const hasWriteAuthQuery = useAuthorizations(
'K8sResourcePoolDetailsW',
undefined,
true
);
const columns = useColumns();
useRepeater(tableState.autoRefreshRate, onRefresh);
const filteredDataset = tableState.showSystemResources
? dataset
: dataset.filter((item) => !item.Namespace.IsSystem);
return (
<Datatable
data-cy="k8sNamespace-namespaceTable"
dataset={filteredDataset}
columns={columns}
settingsManager={tableState}
title="Namespaces"
titleIcon={Layers}
getRowId={(item) => item.Namespace.Id}
isRowSelectable={({ original: item }) =>
hasWriteAuthQuery.authorized &&
!item.Namespace.IsSystem &&
!isDefaultNamespace(item.Namespace.Name)
}
renderTableActions={(selectedItems) => (
<Authorized authorizations="K8sResourcePoolDetailsW" adminOnlyCE>
<DeleteButton
onClick={() => onRemove(selectedItems)}
disabled={selectedItems.length === 0}
/>
<AddButton color="secondary">Add with form</AddButton>
<CreateFromManifestButton />
</Authorized>
)}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
description={
<SystemResourceDescription
showSystemResources={tableState.showSystemResources}
/>
}
/>
);
}

@ -0,0 +1,57 @@
import { CellContext } from '@tanstack/react-table';
import { Users } from 'lucide-react';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { Environment } from '@/react/portainer/environments/types';
import { Link } from '@@/Link';
import { Button } from '@@/buttons';
import { NamespaceViewModel } from '../types';
import { isDefaultNamespace } from '../../isDefaultNamespace';
import { helper } from './helper';
export const actions = helper.display({
header: 'Actions',
cell: Cell,
});
function Cell({
row: { original: item },
}: CellContext<NamespaceViewModel, unknown>) {
const environmentQuery = useCurrentEnvironment();
if (!environmentQuery.data) {
return null;
}
if (!canManageAccess(item, environmentQuery.data)) {
return '-';
}
return (
<Button
as={Link}
color="link"
props={{
to: 'kubernetes.resourcePools.resourcePool.access',
params: { id: item.Namespace.Name },
}}
icon={Users}
>
Manage access
</Button>
);
function canManageAccess(item: NamespaceViewModel, environment: Environment) {
const name = item.Namespace.Name;
const isSystem = item.Namespace.IsSystem;
return (
!isSystem &&
(!isDefaultNamespace(name) ||
environment.Kubernetes.Configuration.RestrictDefaultNamespace)
);
}
}

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { NamespaceViewModel } from '../types';
export const helper = createColumnHelper<NamespaceViewModel>();

@ -0,0 +1,95 @@
import _ from 'lodash';
import { useMemo } from 'react';
import { isoDate } from '@/portainer/filters/filters';
import { useAuthorizations } from '@/react/hooks/useUser';
import { Link } from '@@/Link';
import { StatusBadge } from '@@/StatusBadge';
import { Badge } from '@@/Badge';
import { SystemBadge } from '@@/Badge/SystemBadge';
import { helper } from './helper';
import { actions } from './actions';
export function useColumns() {
const hasAuthQuery = useAuthorizations(
'K8sResourcePoolsAccessManagementRW',
undefined,
true
);
return useMemo(
() =>
_.compact([
helper.accessor('Namespace.Name', {
header: 'Name',
id: 'Name',
cell: ({ getValue, row: { original: item } }) => {
const name = getValue();
return (
<>
<Link
to="kubernetes.resourcePools.resourcePool"
params={{
id: name,
}}
>
{name}
</Link>
{item.Namespace.IsSystem && (
<span className="ml-2">
<SystemBadge />
</span>
)}
</>
);
},
}),
helper.accessor('Namespace.Status', {
header: 'Status',
cell({ getValue }) {
const status = getValue();
return <StatusBadge color={getColor(status)}>{status}</StatusBadge>;
function getColor(status: string) {
switch (status.toLowerCase()) {
case 'active':
return 'success';
case 'terminating':
return 'danger';
default:
return 'info';
}
}
},
}),
helper.accessor('Quota', {
cell({ getValue }) {
const quota = getValue();
if (!quota) {
return '-';
}
return <Badge type="warn">Enabled</Badge>;
},
}),
helper.accessor('Namespace.CreationDate', {
header: 'Created',
cell({ row: { original: item } }) {
return (
<>
{isoDate(item.Namespace.CreationDate)}{' '}
{item.Namespace.ResourcePoolOwner
? ` by ${item.Namespace.ResourcePoolOwner}`
: ''}
</>
);
},
}),
hasAuthQuery.authorized && actions,
]),
[hasAuthQuery.authorized]
);
}

@ -0,0 +1,11 @@
export interface NamespaceViewModel {
Namespace: {
Id: string;
Name: string;
Status: string;
CreationDate: number;
ResourcePoolOwner: string;
IsSystem: boolean;
};
Quota: number;
}

@ -0,0 +1,5 @@
export const KUBERNETES_DEFAULT_NAMESPACE = 'default';
export function isDefaultNamespace(namespace: string) {
return namespace === KUBERNETES_DEFAULT_NAMESPACE;
}
Loading…
Cancel
Save