1
0
Fork 0
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:
Ali 2024-10-25 12:28:05 +13:00 committed by GitHub
parent cc75167437
commit 959c527be7
42 changed files with 1378 additions and 1293 deletions

View file

@ -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(

View file

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

View file

@ -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();
},
});
}
}

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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>
</>
)}
</>
);
}

View file

@ -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]);
}