mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
refactor(apps): migrate applications view to react [r8s-124] (#28)
This commit is contained in:
parent
cc75167437
commit
959c527be7
42 changed files with 1378 additions and 1293 deletions
|
@ -1,28 +1,29 @@
|
|||
import { useMemo } from 'react';
|
||||
import { BoxIcon } from 'lucide-react';
|
||||
import { groupBy, partition } from 'lodash';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { AddButton } from '@@/buttons';
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
|
||||
import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter';
|
||||
import { Namespace } from '../ApplicationsStacksDatatable/types';
|
||||
import { useApplications } from '../../application.queries';
|
||||
import { PodKubernetesInstanceLabel, PodManagedByLabel } from '../../constants';
|
||||
import { useApplications } from '../../queries/useApplications';
|
||||
import { ApplicationsTableSettings } from '../useKubeAppsTableStore';
|
||||
import { useDeleteApplicationsMutation } from '../../queries/useDeleteApplicationsMutation';
|
||||
import { getStacksFromApplications } from '../ApplicationsStacksDatatable/getStacksFromApplications';
|
||||
|
||||
import { Application, ApplicationRowData, ConfigKind } from './types';
|
||||
import { useColumns } from './useColumns';
|
||||
|
@ -31,35 +32,30 @@ import { SubRow } from './SubRow';
|
|||
import { HelmInsightsBox } from './HelmInsightsBox';
|
||||
|
||||
export function ApplicationsDatatable({
|
||||
onRefresh,
|
||||
onRemove,
|
||||
namespace = '',
|
||||
namespaces,
|
||||
onNamespaceChange,
|
||||
hideStacks,
|
||||
tableState,
|
||||
hideStacks = false,
|
||||
}: {
|
||||
onRefresh: () => void;
|
||||
onRemove: (selectedItems: Application[]) => void;
|
||||
namespace?: string;
|
||||
namespaces: Array<Namespace>;
|
||||
onNamespaceChange(namespace: string): void;
|
||||
hideStacks: boolean;
|
||||
hideStacks?: boolean;
|
||||
tableState: ApplicationsTableSettings & {
|
||||
setSearch: (value: string) => void;
|
||||
search: string;
|
||||
};
|
||||
}) {
|
||||
const envId = useEnvironmentId();
|
||||
const envQuery = useCurrentEnvironment();
|
||||
const namespaceListQuery = useNamespacesQuery(envId);
|
||||
|
||||
const tableState = useKubeStore('kubernetes.applications', 'Name');
|
||||
useRepeater(tableState.autoRefreshRate, onRefresh);
|
||||
|
||||
const hasWriteAuthQuery = useAuthorizations(
|
||||
const router = useRouter();
|
||||
const environmentId = useEnvironmentId();
|
||||
const restrictSecretsQuery = useEnvironment(
|
||||
environmentId,
|
||||
(env) => env.Kubernetes.Configuration.RestrictSecrets
|
||||
);
|
||||
const namespaceListQuery = useNamespacesQuery(environmentId);
|
||||
const { authorized: hasWriteAuth } = useAuthorizations(
|
||||
'K8sApplicationsW',
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
const applicationsQuery = useApplications(envId, {
|
||||
const applicationsQuery = useApplications(environmentId, {
|
||||
refetchInterval: tableState.autoRefreshRate * 1000,
|
||||
namespace,
|
||||
namespace: tableState.namespace,
|
||||
withDependencies: true,
|
||||
});
|
||||
const applications = useApplicationsRowData(applicationsQuery.data);
|
||||
|
@ -69,20 +65,25 @@ export function ApplicationsDatatable({
|
|||
(application) =>
|
||||
!isSystemNamespace(application.ResourcePool, namespaceListQuery.data)
|
||||
);
|
||||
const stacks = getStacksFromApplications(filteredApplications);
|
||||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||
environmentId,
|
||||
stacks,
|
||||
reportStacks: false,
|
||||
});
|
||||
|
||||
const columns = useColumns(hideStacks);
|
||||
|
||||
return (
|
||||
<ExpandableDatatable
|
||||
data-cy="k8sApp-appTable"
|
||||
noWidget
|
||||
dataset={filteredApplications ?? []}
|
||||
settingsManager={tableState}
|
||||
columns={columns}
|
||||
title="Applications"
|
||||
titleIcon={BoxIcon}
|
||||
isLoading={applicationsQuery.isLoading}
|
||||
disableSelect={!hasWriteAuthQuery.authorized}
|
||||
disableSelect={!hasWriteAuth}
|
||||
isRowSelectable={(row) =>
|
||||
!isSystemNamespace(row.original.ResourcePool, namespaceListQuery.data)
|
||||
}
|
||||
|
@ -91,25 +92,22 @@ export function ApplicationsDatatable({
|
|||
<SubRow
|
||||
item={row.original}
|
||||
hideStacks={hideStacks}
|
||||
areSecretsRestricted={
|
||||
envQuery.data?.Kubernetes.Configuration.RestrictSecrets || false
|
||||
}
|
||||
areSecretsRestricted={!!restrictSecretsQuery.data}
|
||||
/>
|
||||
)}
|
||||
renderTableActions={(selectedItems) =>
|
||||
hasWriteAuthQuery.authorized && (
|
||||
hasWriteAuth && (
|
||||
<>
|
||||
<DeleteButton
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
disabled={selectedItems.length === 0}
|
||||
isLoading={removeApplicationsMutation.isLoading}
|
||||
confirmMessage="Do you want to remove the selected application(s)?"
|
||||
onConfirmed={() => onRemove(selectedItems)}
|
||||
onConfirmed={() => handleRemoveApplications(selectedItems)}
|
||||
/>
|
||||
|
||||
<AddButton data-cy="k8sApp-addApplicationButton" color="secondary">
|
||||
Add with form
|
||||
</AddButton>
|
||||
|
||||
<CreateFromManifestButton data-cy="k8sApp-deployFromManifestButton" />
|
||||
</>
|
||||
)
|
||||
|
@ -123,18 +121,16 @@ export function ApplicationsDatatable({
|
|||
<div className="w-full">
|
||||
<div className="min-w-[140px] float-right">
|
||||
<NamespaceFilter
|
||||
namespaces={namespaces}
|
||||
value={namespace}
|
||||
onChange={onNamespaceChange}
|
||||
namespaces={namespaceListQuery.data ?? []}
|
||||
value={tableState.namespace}
|
||||
onChange={tableState.setNamespace}
|
||||
showSystem={tableState.showSystemResources}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<SystemResourceDescription
|
||||
showSystemResources={tableState.showSystemResources}
|
||||
/>
|
||||
|
||||
<div className="w-fit">
|
||||
<HelmInsightsBox />
|
||||
</div>
|
||||
|
@ -143,6 +139,14 @@ export function ApplicationsDatatable({
|
|||
}
|
||||
/>
|
||||
);
|
||||
|
||||
function handleRemoveApplications(applications: ApplicationRowData[]) {
|
||||
removeApplicationsMutation.mutate(applications, {
|
||||
onSuccess: () => {
|
||||
router.stateService.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function useApplicationsRowData(
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { ApplicationsDatatable } from './ApplicationsDatatable';
|
|
@ -1,52 +1,44 @@
|
|||
import { List } from 'lucide-react';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { useApplications } from '../../application.queries';
|
||||
import { useApplications } from '../../queries/useApplications';
|
||||
import { ApplicationsTableSettings } from '../useKubeAppsTableStore';
|
||||
import { useDeleteApplicationsMutation } from '../../queries/useDeleteApplicationsMutation';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { SubRows } from './SubRows';
|
||||
import { Namespace, Stack } from './types';
|
||||
import { NamespaceFilter } from './NamespaceFilter';
|
||||
import { TableActions } from './TableActions';
|
||||
import { getStacksFromApplications } from './getStacksFromApplications';
|
||||
|
||||
const storageKey = 'kubernetes.applications.stacks';
|
||||
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
interface Props {
|
||||
onRemove(selectedItems: Array<Stack>): void;
|
||||
namespace?: string;
|
||||
namespaces: Array<Namespace>;
|
||||
onNamespaceChange(namespace: string): void;
|
||||
}
|
||||
import { Stack } from './types';
|
||||
|
||||
export function ApplicationsStacksDatatable({
|
||||
onRemove,
|
||||
namespace = '',
|
||||
namespaces,
|
||||
onNamespaceChange,
|
||||
}: Props) {
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const envId = useEnvironmentId();
|
||||
const applicationsQuery = useApplications(envId, {
|
||||
tableState,
|
||||
}: {
|
||||
tableState: ApplicationsTableSettings & {
|
||||
setSearch: (value: string) => void;
|
||||
search: string;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const environmentId = useEnvironmentId();
|
||||
const namespaceListQuery = useNamespacesQuery(environmentId);
|
||||
const { authorized: hasWriteAuth } = useAuthorizations('K8sApplicationsW');
|
||||
const applicationsQuery = useApplications(environmentId, {
|
||||
refetchInterval: tableState.autoRefreshRate * 1000,
|
||||
namespace,
|
||||
namespace: tableState.namespace,
|
||||
withDependencies: true,
|
||||
});
|
||||
const namespaceListQuery = useNamespacesQuery(envId);
|
||||
const applications = applicationsQuery.data ?? [];
|
||||
const filteredApplications = tableState.showSystemResources
|
||||
? applications
|
||||
|
@ -54,10 +46,12 @@ export function ApplicationsStacksDatatable({
|
|||
(item) =>
|
||||
!isSystemNamespace(item.ResourcePool, namespaceListQuery.data ?? [])
|
||||
);
|
||||
|
||||
const { authorized } = useAuthorizations('K8sApplicationsW');
|
||||
|
||||
const stacks = getStacksFromApplications(filteredApplications);
|
||||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||
environmentId,
|
||||
stacks,
|
||||
reportStacks: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpandableDatatable
|
||||
|
@ -68,18 +62,17 @@ export function ApplicationsStacksDatatable({
|
|||
isLoading={applicationsQuery.isLoading || namespaceListQuery.isLoading}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
disableSelect={!authorized}
|
||||
disableSelect={!hasWriteAuth}
|
||||
renderSubRow={(row) => (
|
||||
<SubRows stack={row.original} span={row.getVisibleCells().length} />
|
||||
)}
|
||||
noWidget
|
||||
description={
|
||||
<div className="w-full">
|
||||
<div className="float-right mr-2 min-w-[140px]">
|
||||
<NamespaceFilter
|
||||
namespaces={namespaces}
|
||||
value={namespace}
|
||||
onChange={onNamespaceChange}
|
||||
namespaces={namespaceListQuery.data ?? []}
|
||||
value={tableState.namespace}
|
||||
onChange={tableState.setNamespace}
|
||||
showSystem={tableState.showSystemResources}
|
||||
/>
|
||||
</div>
|
||||
|
@ -92,7 +85,14 @@ export function ApplicationsStacksDatatable({
|
|||
</div>
|
||||
}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<TableActions selectedItems={selectedItems} onRemove={onRemove} />
|
||||
<Authorized authorizations="K8sApplicationsW">
|
||||
<DeleteButton
|
||||
confirmMessage="Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s)."
|
||||
disabled={selectedItems.length === 0}
|
||||
onConfirmed={() => handleRemoveStacks(selectedItems)}
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
/>
|
||||
</Authorized>
|
||||
)}
|
||||
renderTableSettings={() => (
|
||||
<TableSettingsMenu>
|
||||
|
@ -103,4 +103,13 @@ export function ApplicationsStacksDatatable({
|
|||
data-cy="applications-stacks-datatable"
|
||||
/>
|
||||
);
|
||||
|
||||
function handleRemoveStacks(selectedItems: Stack[]) {
|
||||
const applications = selectedItems.flatMap((stack) => stack.Applications);
|
||||
removeApplicationsMutation.mutate(applications, {
|
||||
onSuccess: () => {
|
||||
router.stateService.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { Filter } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { PortainerNamespace } from '@/react/kubernetes/namespaces/types';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Select } from '@@/form-components/Input';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
|
||||
import { Namespace } from './types';
|
||||
|
||||
function transformNamespaces(namespaces: Namespace[], showSystem?: boolean) {
|
||||
function transformNamespaces(
|
||||
namespaces: PortainerNamespace[],
|
||||
showSystem?: boolean
|
||||
) {
|
||||
const transformedNamespaces = namespaces.map(({ Name, IsSystem }) => ({
|
||||
label: IsSystem ? `${Name} - system` : Name,
|
||||
value: Name,
|
||||
|
@ -26,7 +29,7 @@ export function NamespaceFilter({
|
|||
onChange,
|
||||
showSystem,
|
||||
}: {
|
||||
namespaces: Namespace[];
|
||||
namespaces: PortainerNamespace[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
showSystem?: boolean;
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { Authorized } from '@/react/hooks/useUser';
|
||||
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { Stack } from './types';
|
||||
|
||||
export function TableActions({
|
||||
selectedItems,
|
||||
onRemove,
|
||||
}: {
|
||||
selectedItems: Array<Stack>;
|
||||
onRemove: (selectedItems: Array<Stack>) => void;
|
||||
}) {
|
||||
return (
|
||||
<Authorized authorizations="K8sApplicationsW">
|
||||
<DeleteButton
|
||||
confirmMessage="Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s)."
|
||||
disabled={selectedItems.length === 0}
|
||||
onConfirmed={() => onRemove(selectedItems)}
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
/>
|
||||
</Authorized>
|
||||
);
|
||||
}
|
|
@ -12,13 +12,6 @@ export interface TableSettings
|
|||
RefreshableTableSettings,
|
||||
SystemResourcesTableSettings {}
|
||||
|
||||
export interface Namespace {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Yaml: string;
|
||||
IsSystem?: boolean;
|
||||
}
|
||||
|
||||
export type Stack = {
|
||||
Name: string;
|
||||
ResourcePool: string;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { BoxIcon, List } from 'lucide-react';
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Tab, WidgetTabs, findSelectedTabIndex } from '@@/Widget/WidgetTabs';
|
||||
|
||||
import { ApplicationsDatatable } from './ApplicationsDatatable';
|
||||
import { ApplicationsStacksDatatable } from './ApplicationsStacksDatatable';
|
||||
import { useKubeAppsTableStore } from './useKubeAppsTableStore';
|
||||
|
||||
export function ApplicationsView() {
|
||||
const tableState = useKubeAppsTableStore('kubernetes.applications', 'Name');
|
||||
const hideStacksQuery = usePublicSettings({
|
||||
select: (settings) =>
|
||||
settings.GlobalDeploymentOptions.hideStacksFunctionality,
|
||||
});
|
||||
const hideStacks = hideStacksQuery.isLoading || !!hideStacksQuery.data;
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
name: 'Applications',
|
||||
icon: BoxIcon,
|
||||
widget: <ApplicationsDatatable tableState={tableState} />,
|
||||
selectedTabParam: 'applications',
|
||||
},
|
||||
{
|
||||
name: 'Stacks',
|
||||
icon: List,
|
||||
widget: <ApplicationsStacksDatatable tableState={tableState} />,
|
||||
selectedTabParam: 'stacks',
|
||||
},
|
||||
];
|
||||
|
||||
const currentTabIndex = findSelectedTabIndex(
|
||||
useCurrentStateAndParams(),
|
||||
tabs
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Application list" breadcrumbs="Applications" reload />
|
||||
{hideStacks ? (
|
||||
<ApplicationsDatatable tableState={tableState} hideStacks />
|
||||
) : (
|
||||
<>
|
||||
<WidgetTabs tabs={tabs} currentTabIndex={currentTabIndex} />
|
||||
<div className="content">{tabs[currentTabIndex].widget}</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
|
||||
import {
|
||||
refreshableSettings,
|
||||
createPersistedStore,
|
||||
ZustandSetFunc,
|
||||
} from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { systemResourcesSettings } from '../../datatables/SystemResourcesSettings';
|
||||
|
||||
interface NamespaceSelectSettings {
|
||||
namespace: string;
|
||||
setNamespace: (namespace: string) => void;
|
||||
}
|
||||
export type ApplicationsTableSettings = NamespaceSelectSettings & TableSettings;
|
||||
|
||||
export function namespaceSelectSettings<T extends NamespaceSelectSettings>(
|
||||
set: ZustandSetFunc<T>,
|
||||
namespace = 'default'
|
||||
): NamespaceSelectSettings {
|
||||
return {
|
||||
namespace,
|
||||
setNamespace: (namespace: string) => set((s) => ({ ...s, namespace })),
|
||||
};
|
||||
}
|
||||
|
||||
export function createStore<T extends ApplicationsTableSettings>(
|
||||
storageKey: string,
|
||||
initialSortBy?: string | { id: string; desc: boolean },
|
||||
create: (
|
||||
set: ZustandSetFunc<T>
|
||||
) => Omit<T, keyof ApplicationsTableSettings> = () => ({}) as T
|
||||
) {
|
||||
return createPersistedStore<T>(
|
||||
storageKey,
|
||||
initialSortBy,
|
||||
(set) =>
|
||||
({
|
||||
...refreshableSettings(set),
|
||||
...systemResourcesSettings(set),
|
||||
...namespaceSelectSettings(set),
|
||||
...create(set),
|
||||
}) as T
|
||||
);
|
||||
}
|
||||
|
||||
export function useKubeAppsTableStore<T extends ApplicationsTableSettings>(
|
||||
...args: Parameters<typeof createStore<T>>
|
||||
) {
|
||||
const [store] = useState(() => createStore(...args));
|
||||
return useTableState(store, args[0]);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue