Compare commits
1 Commits
docs/dev-s
...
fix/stack-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f379e8057e |
@@ -4,6 +4,13 @@ import { createRowContext } from '@@/datatables/RowContext';
|
||||
|
||||
interface RowContextState {
|
||||
environment: Environment;
|
||||
// When the containers datatable is rendered inside a stack, these are the
|
||||
// current stack route params (name/stackId/type/regular/external/orphaned/
|
||||
// orphanedRunning/tab). They let the Quick Actions column build links to the
|
||||
// stack-scoped container sub-tab states so the stack breadcrumb trail is
|
||||
// preserved. Undefined for the global containers list (keeps global links).
|
||||
// Raw route params (strings), consumed by buildStackLinkParams downstream.
|
||||
stackRouteParams?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||
|
||||
@@ -3,10 +3,12 @@ import { CellContext } from '@tanstack/react-table';
|
||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { ContainerListViewModel } from '@/react/docker/containers/types';
|
||||
import { buildStackContainerLinkParams } from '@/react/docker/containers/ItemView/containerBreadcrumbs';
|
||||
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
import { useRowContext } from '../RowContext';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -20,9 +22,20 @@ function QuickActionsCell({
|
||||
row: { original: container },
|
||||
}: CellContext<ContainerListViewModel, unknown>) {
|
||||
const settings = useTableSettings<TableSettings>();
|
||||
const { stackRouteParams } = useRowContext();
|
||||
|
||||
const { hiddenQuickActions = [] } = settings;
|
||||
|
||||
// When rendered inside a stack, build the params for the stack-scoped
|
||||
// container sub-tab states so the stack breadcrumb trail is preserved.
|
||||
// Undefined in the global containers list, which keeps the global links.
|
||||
const containerStackLinkParams = stackRouteParams
|
||||
? buildStackContainerLinkParams(
|
||||
{ ...stackRouteParams, nodeName: container.NodeName },
|
||||
container.Id
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const wrapperState = {
|
||||
showQuickActionAttach: !hiddenQuickActions.includes('attach'),
|
||||
showQuickActionExec: !hiddenQuickActions.includes('exec'),
|
||||
@@ -57,6 +70,7 @@ function QuickActionsCell({
|
||||
nodeName={container.NodeName}
|
||||
status={container.Status}
|
||||
state={wrapperState}
|
||||
stackLinkParams={containerStackLinkParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { ContainerStatus } from '@/react/docker/containers/types';
|
||||
import { buildStackContainerLinkParams } from '@/react/docker/containers/ItemView/containerBreadcrumbs';
|
||||
|
||||
import {
|
||||
ContainerQuickActions,
|
||||
QuickActionsState,
|
||||
} from './ContainerQuickActions';
|
||||
|
||||
// Render the Link's target state and params as data attributes so the tests can
|
||||
// assert on them without a full ui-router state tree.
|
||||
vi.mock('@@/Link', () => ({
|
||||
Link: ({
|
||||
children,
|
||||
to,
|
||||
params,
|
||||
'data-cy': dataCy,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
to: string;
|
||||
params?: Record<string, unknown>;
|
||||
'data-cy'?: string;
|
||||
}) => (
|
||||
<a
|
||||
href="/"
|
||||
data-cy={dataCy}
|
||||
data-to={to}
|
||||
data-params={JSON.stringify(params)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Authorizations are covered elsewhere; render the children unconditionally.
|
||||
vi.mock('@/react/hooks/useUser', () => ({
|
||||
Authorized: ({ children }: { children: ReactNode }) => children,
|
||||
}));
|
||||
|
||||
const allOn: QuickActionsState = {
|
||||
showQuickActionAttach: true,
|
||||
showQuickActionExec: true,
|
||||
showQuickActionInspect: true,
|
||||
showQuickActionLogs: true,
|
||||
showQuickActionStats: true,
|
||||
};
|
||||
|
||||
const containerId = 'abc123';
|
||||
const nodeName = 'node-1';
|
||||
|
||||
const tabs = ['logs', 'inspect', 'stats', 'exec', 'attach'] as const;
|
||||
|
||||
// Look the rendered Link up by the data-cy our Link mock sets.
|
||||
function byDataCy(tab: (typeof tabs)[number]) {
|
||||
return document.querySelector<HTMLElement>(
|
||||
`[data-cy="container-${tab}-${containerId}"]`
|
||||
);
|
||||
}
|
||||
|
||||
test('without stack context, links target the global container states', () => {
|
||||
render(
|
||||
<ContainerQuickActions
|
||||
containerId={containerId}
|
||||
nodeName={nodeName}
|
||||
status={ContainerStatus.Running}
|
||||
state={allOn}
|
||||
/>
|
||||
);
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const link = byDataCy(tab);
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.getAttribute('data-to')).toBe(
|
||||
`docker.containers.container.${tab}`
|
||||
);
|
||||
expect(JSON.parse(link?.getAttribute('data-params') || '{}')).toEqual({
|
||||
id: containerId,
|
||||
nodeName,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('with stack context, links target the stack-scoped container states with stack params', () => {
|
||||
const stackRouteParams = {
|
||||
name: 'my-stack',
|
||||
stackId: '7',
|
||||
type: '2',
|
||||
regular: 'true',
|
||||
tab: 'containers',
|
||||
};
|
||||
const stackLinkParams = buildStackContainerLinkParams(
|
||||
{ ...stackRouteParams, nodeName },
|
||||
containerId
|
||||
);
|
||||
|
||||
render(
|
||||
<ContainerQuickActions
|
||||
containerId={containerId}
|
||||
nodeName={nodeName}
|
||||
status={ContainerStatus.Running}
|
||||
state={allOn}
|
||||
stackLinkParams={stackLinkParams}
|
||||
/>
|
||||
);
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const link = byDataCy(tab);
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.getAttribute('data-to')).toBe(
|
||||
`docker.stacks.stack.container.${tab}`
|
||||
);
|
||||
// Every sub-tab shares the same stack params + container id/nodeName so the
|
||||
// stack breadcrumb trail is preserved on navigation.
|
||||
expect(JSON.parse(link?.getAttribute('data-params') || '{}')).toEqual({
|
||||
name: 'my-stack',
|
||||
stackId: '7',
|
||||
type: '2',
|
||||
regular: 'true',
|
||||
orphaned: undefined,
|
||||
orphanedRunning: undefined,
|
||||
tab: 'containers',
|
||||
id: containerId,
|
||||
nodeName,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,10 @@ import clsx from 'clsx';
|
||||
import { BarChart, FileText, Info, Paperclip, Terminal } from 'lucide-react';
|
||||
|
||||
import { ContainerStatus } from '@/react/docker/containers/types';
|
||||
import {
|
||||
STACK_CONTAINER_STATE_NAME,
|
||||
StackContainerLinkParams,
|
||||
} from '@/react/docker/containers/ItemView/containerBreadcrumbs';
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
@@ -22,11 +26,16 @@ export function ContainerQuickActions({
|
||||
containerId,
|
||||
nodeName,
|
||||
state,
|
||||
stackLinkParams,
|
||||
}: {
|
||||
containerId: string;
|
||||
nodeName: string;
|
||||
status: ContainerStatus;
|
||||
state: QuickActionsState;
|
||||
// When provided (container opened from a stack), the quick actions link to
|
||||
// the stack-scoped container sub-tab states with these params so the stack
|
||||
// breadcrumb trail is preserved. Otherwise they link to the global states.
|
||||
stackLinkParams?: StackContainerLinkParams;
|
||||
}) {
|
||||
const isActive =
|
||||
!!status &&
|
||||
@@ -37,13 +46,23 @@ export function ContainerQuickActions({
|
||||
ContainerStatus.Unhealthy,
|
||||
].includes(status);
|
||||
|
||||
// Build the target ui-router state for a given sub-tab: the stack-scoped
|
||||
// state when inside a stack, the global container state otherwise. Both share
|
||||
// the same tab suffix (logs/inspect/stats/exec/attach).
|
||||
function linkTo(tab: string) {
|
||||
return stackLinkParams
|
||||
? `${STACK_CONTAINER_STATE_NAME}.${tab}`
|
||||
: `docker.containers.container.${tab}`;
|
||||
}
|
||||
const linkParams = stackLinkParams ?? { id: containerId, nodeName };
|
||||
|
||||
return (
|
||||
<div className={clsx('space-x-1', styles.root)}>
|
||||
{state.showQuickActionLogs && (
|
||||
<Authorized authorizations="DockerContainerLogs">
|
||||
<Link
|
||||
to="docker.containers.container.logs"
|
||||
params={{ id: containerId, nodeName }}
|
||||
to={linkTo('logs')}
|
||||
params={linkParams}
|
||||
title="Logs"
|
||||
data-cy={`container-logs-${containerId}`}
|
||||
>
|
||||
@@ -55,8 +74,8 @@ export function ContainerQuickActions({
|
||||
{state.showQuickActionInspect && (
|
||||
<Authorized authorizations="DockerContainerInspect">
|
||||
<Link
|
||||
to="docker.containers.container.inspect"
|
||||
params={{ id: containerId, nodeName }}
|
||||
to={linkTo('inspect')}
|
||||
params={linkParams}
|
||||
title="Inspect"
|
||||
data-cy={`container-inspect-${containerId}`}
|
||||
>
|
||||
@@ -68,8 +87,8 @@ export function ContainerQuickActions({
|
||||
{state.showQuickActionStats && isActive && (
|
||||
<Authorized authorizations="DockerContainerStats">
|
||||
<Link
|
||||
to="docker.containers.container.stats"
|
||||
params={{ id: containerId, nodeName }}
|
||||
to={linkTo('stats')}
|
||||
params={linkParams}
|
||||
title="Stats"
|
||||
data-cy={`container-stats-${containerId}`}
|
||||
>
|
||||
@@ -81,8 +100,8 @@ export function ContainerQuickActions({
|
||||
{state.showQuickActionExec && isActive && (
|
||||
<Authorized authorizations="DockerExecStart">
|
||||
<Link
|
||||
to="docker.containers.container.exec"
|
||||
params={{ id: containerId, nodeName }}
|
||||
to={linkTo('exec')}
|
||||
params={linkParams}
|
||||
title="Exec Console"
|
||||
data-cy={`container-exec-${containerId}`}
|
||||
>
|
||||
@@ -94,8 +113,8 @@ export function ContainerQuickActions({
|
||||
{state.showQuickActionAttach && isActive && (
|
||||
<Authorized authorizations="DockerContainerAttach">
|
||||
<Link
|
||||
to="docker.containers.container.attach"
|
||||
params={{ id: containerId, nodeName }}
|
||||
to={linkTo('attach')}
|
||||
params={linkParams}
|
||||
title="Attach Console"
|
||||
data-cy={`container-attach-${containerId}`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box } from 'lucide-react';
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
import { ContainerListViewModel } from '@/react/docker/containers/types';
|
||||
import { createStore } from '@/react/docker/containers/ListView/ContainersDatatable/datatable-store';
|
||||
@@ -42,6 +43,9 @@ export interface Props {
|
||||
export function StackContainersDatatable({ stackName }: Props) {
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
// Current stack route params, forwarded to the Quick Actions column so it can
|
||||
// link to the stack-scoped container sub-tab states and keep the stack trail.
|
||||
const { params: stackRouteParams } = useCurrentStateAndParams();
|
||||
|
||||
const isGPUsColumnVisible = useShowGPUsColumn(environmentQuery.data);
|
||||
const columns = useColumns(false, isGPUsColumnVisible);
|
||||
@@ -60,7 +64,7 @@ export function StackContainersDatatable({ stackName }: Props) {
|
||||
const environment = environmentQuery.data;
|
||||
|
||||
return (
|
||||
<RowProvider context={{ environment }}>
|
||||
<RowProvider context={{ environment, stackRouteParams }}>
|
||||
<TableSettingsProvider settings={settingsStore}>
|
||||
<Datatable
|
||||
title="Containers"
|
||||
|
||||
Reference in New Issue
Block a user