Skip to content

Commit cbed3a9

Browse files
fix(deployments): show error indicator in collapsed sections (Issue #2130) (#2146)
1 parent c10f02d commit cbed3a9

File tree

16 files changed

+119
-23
lines changed

16 files changed

+119
-23
lines changed

apps/ai-dial-admin/src/components/Common/Accordion/Accordion.spec.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,14 @@ describe('Common components :: Accordion', () => {
3939
fireEvent.click(toggleButton);
4040
expect(content).toHaveClass('hidden');
4141
});
42+
43+
test('render error indicator', () => {
44+
render(
45+
<Accordion title={'title'} errorIndicator={true}>
46+
<div>Child content</div>
47+
</Accordion>,
48+
);
49+
50+
expect(screen.getByRole('status')).toBeInTheDocument();
51+
});
4252
});

apps/ai-dial-admin/src/components/Common/Accordion/Accordion.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import classNames from 'classnames';
33
import { IconChevronDown, IconChevronRight } from '@tabler/icons-react';
44

55
import { BASE_BUTTON_ICON_PROPS } from '@/src/constants/main-layout';
6+
import { ErrorI18nKey } from '@/src/constants/i18n';
7+
import { useI18n } from '@/src/locales/client';
68

79
interface Props {
810
title?: string;
@@ -12,6 +14,7 @@ interface Props {
1214
header?: ReactElement<{ isCollapsed: boolean }>;
1315
containerClassName?: string;
1416
contentClassName?: string;
17+
errorIndicator?: boolean;
1518
}
1619

1720
const Accordion: FC<Props> = ({
@@ -22,7 +25,9 @@ const Accordion: FC<Props> = ({
2225
actionButtons,
2326
contentClassName,
2427
containerClassName,
28+
errorIndicator,
2529
}) => {
30+
const t = useI18n();
2631
const [isCollapsed, setIsCollapsed] = useState(collapsed);
2732

2833
const toggleCollapse = useCallback(() => {
@@ -42,6 +47,13 @@ const Accordion: FC<Props> = ({
4247
<button className="flex items-center w-full" onClick={toggleCollapse}>
4348
{icon}
4449
<h3 className="mx-2">{title}</h3>
50+
{errorIndicator && (
51+
<span
52+
role="status"
53+
className="flex w-2 h-2 rounded no-user-select bg-red-400"
54+
aria-label={t(ErrorI18nKey.Error)}
55+
/>
56+
)}
4557
</button>
4658
{actionButtons}
4759
</div>

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerAutoscaling.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useCallback, useEffect, useState } from 'react';
1+
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
22
import { DialNumberInputField, DialSelectField } from '@epam/ai-dial-ui-kit';
33

44
import { AutoscalingStrategy, Container } from '@/src/models/deployments/containers';
@@ -10,7 +10,7 @@ import { AUTOSCALE_OPTIONS } from '@/src/constants/deployments/containers';
1010
import { useI18n } from '@/src/locales/client';
1111

1212
import Accordion from '@/src/components/Common/Accordion/Accordion';
13-
import { isEditDisabled } from '@/src/utils/deployments/containers';
13+
import { isEditDisabled, isErrorPresent } from '@/src/utils/deployments/containers';
1414

1515
interface Props {
1616
container: Container;
@@ -19,10 +19,19 @@ interface Props {
1919

2020
const ContainerAutoscaling: FC<Props> = ({ container, setContainer }) => {
2121
const t = useI18n();
22-
const { dispatch, resetCounter } = useSaveValidationContext();
23-
const scalingOptions = AUTOSCALE_OPTIONS(t);
22+
const { dispatch, resetCounter, isValid, errorFields } = useSaveValidationContext();
2423

24+
const scalingOptions = useMemo(() => AUTOSCALE_OPTIONS(t), [t]);
2525
const [replicasError, setReplicasError] = useState<FieldError | null>(null);
26+
const [isSectionInvalid, setSectionInvalid] = useState(false);
27+
28+
useEffect(() => {
29+
if (!isValid) {
30+
setSectionInvalid(isErrorPresent(errorFields, ['scaling']));
31+
} else {
32+
setSectionInvalid(false);
33+
}
34+
}, [errorFields, isValid]);
2635

2736
useEffect(() => {
2837
if (resetCounter || (container.scaling?.maxReplicas && container.scaling.maxReplicas)) {
@@ -108,7 +117,7 @@ const ContainerAutoscaling: FC<Props> = ({ container, setContainer }) => {
108117
);
109118

110119
return (
111-
<Accordion title={t(EntityFieldsI18nKey.Autoscaling)}>
120+
<Accordion title={t(EntityFieldsI18nKey.Autoscaling)} errorIndicator={isSectionInvalid}>
112121
<div className="flex flex-col gap-6">
113122
<div className="flex gap-4 flex-col lg:flex-row">
114123
<DialSelectField

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerEndpoint.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from 'react';
1+
import { FC, useEffect, useState } from 'react';
22

33
import { EntityFieldsI18nKey } from '@/src/constants/i18n';
44
import { Container } from '@/src/models/deployments/containers';
@@ -9,6 +9,8 @@ import Accordion from '@/src/components/Common/Accordion/Accordion';
99
import Transport from '@/src/components/Deployments/Fields/ContainerEndpoint/Transport';
1010
import Port from '@/src/components/Deployments/Fields/ContainerEndpoint/Port';
1111
import EndpointPath from '@/src/components/Deployments/Fields/ContainerEndpoint/EndpointPath';
12+
import { isErrorPresent } from '@/src/utils/deployments/containers';
13+
import { useSaveValidationContext } from '@/src/context/SaveValidationContext';
1214

1315
interface Props {
1416
container: Container;
@@ -18,9 +20,22 @@ interface Props {
1820

1921
const ContainerEndpoint: FC<Props> = ({ container, setContainer, route }) => {
2022
const t = useI18n();
23+
const { errorFields, isValid } = useSaveValidationContext();
24+
25+
const [isSectionInvalid, setSectionInvalid] = useState(false);
26+
27+
useEffect(() => {
28+
if (!isValid) {
29+
setSectionInvalid(
30+
isErrorPresent(errorFields, ['transport', 'mcpEndpointPath', 'containerGrpcPort', 'containerPort']),
31+
);
32+
} else {
33+
setSectionInvalid(false);
34+
}
35+
}, [errorFields, isValid]);
2136

2237
return (
23-
<Accordion title={t(EntityFieldsI18nKey.EndpointConfiguration)}>
38+
<Accordion title={t(EntityFieldsI18nKey.EndpointConfiguration)} errorIndicator={isSectionInvalid}>
2439
<div className="flex flex-col gap-y-8">
2540
{route === ApplicationRoute.McpContainers && (
2641
<div className="flex gap-4">

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerEndpoint/Port.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const Port: FC<Props> = ({ container, setContainer }) => {
2828
const { containerPort: __, ...rest } = container;
2929
setContainer(rest);
3030
} else {
31-
const error = getPortError(containerPort as number);
31+
const error = getPortError(containerPort as number, t);
3232
setPortError(error);
3333
dispatch({ type: ValidationActionType.SetField, field: 'containerPort', isValid: !error });
3434
setContainer({
@@ -37,7 +37,7 @@ const Port: FC<Props> = ({ container, setContainer }) => {
3737
});
3838
}
3939
},
40-
[container, dispatch, setContainer],
40+
[container, dispatch, setContainer, t],
4141
);
4242

4343
const onGRPCPortChange = useCallback(
@@ -46,7 +46,7 @@ const Port: FC<Props> = ({ container, setContainer }) => {
4646
const { containerGrpcPort: __, ...rest } = container;
4747
setContainer(rest);
4848
} else {
49-
const error = getPortError(containerGrpcPort as number);
49+
const error = getPortError(containerGrpcPort as number, t);
5050
setGrpcPortError(error);
5151
dispatch({ type: ValidationActionType.SetField, field: 'containerGrpcPort', isValid: !error });
5252
setContainer({
@@ -55,24 +55,24 @@ const Port: FC<Props> = ({ container, setContainer }) => {
5555
});
5656
}
5757
},
58-
[container, dispatch, setContainer],
58+
[container, dispatch, setContainer, t],
5959
);
6060

6161
useEffect(() => {
6262
if (resetCounter || container.containerPort) {
63-
const error = getPortError(container.containerPort as number);
63+
const error = getPortError(container.containerPort as number, t);
6464
setPortError(error);
6565
dispatch({ type: ValidationActionType.SetField, field: 'containerPort', isValid: !error });
6666
}
67-
}, [container.containerPort, dispatch, resetCounter]);
67+
}, [container.containerPort, dispatch, resetCounter, t]);
6868

6969
useEffect(() => {
7070
if (resetCounter || container.containerGrpcPort) {
71-
const error = getPortError(container.containerGrpcPort as number);
71+
const error = getPortError(container.containerGrpcPort as number, t);
7272
setGrpcPortError(error);
7373
dispatch({ type: ValidationActionType.SetField, field: 'containerGrpcPort', isValid: !error });
7474
}
75-
}, [container.containerGrpcPort, dispatch, resetCounter]);
75+
}, [container.containerGrpcPort, dispatch, resetCounter, t]);
7676

7777
return (
7878
<div className="flex gap-4">

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerResources.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EntityFieldsI18nKey } from '@/src/constants/i18n';
77
import { FieldError } from '@/src/models/error';
88
import { useSaveValidationContext, ValidationActionType } from '@/src/context/SaveValidationContext';
99
import { getGpuError } from '@/src/utils/deployments/validation';
10-
import { isEditDisabled } from '@/src/utils/deployments/containers';
10+
import { isEditDisabled, isErrorPresent } from '@/src/utils/deployments/containers';
1111
import { useI18n } from '@/src/locales/client';
1212

1313
import Accordion from '@/src/components/Common/Accordion/Accordion';
@@ -22,9 +22,20 @@ interface Props {
2222

2323
const ContainerResources: FC<Props> = ({ container, setContainer, route }) => {
2424
const t = useI18n();
25-
const { dispatch, resetCounter } = useSaveValidationContext();
25+
const { dispatch, resetCounter, errorFields, isValid } = useSaveValidationContext();
2626

2727
const [error, setError] = useState<FieldError | null>(null);
28+
const [isSectionInvalid, setSectionInvalid] = useState(false);
29+
30+
useEffect(() => {
31+
if (!isValid) {
32+
setSectionInvalid(
33+
isErrorPresent(errorFields, ['gpuRequest', 'cpuRequest', 'cpuLimit', 'memoryRequest', 'memoryLimit']),
34+
);
35+
} else {
36+
setSectionInvalid(false);
37+
}
38+
}, [errorFields, isValid]);
2839

2940
const onChangeGpuRequest = useCallback(
3041
(gpuRequest?: string | number) => {
@@ -66,7 +77,7 @@ const ContainerResources: FC<Props> = ({ container, setContainer, route }) => {
6677
}, [container.resources?.requests, dispatch, resetCounter, t]);
6778

6879
return (
69-
<Accordion title={t(EntityFieldsI18nKey.Resources)}>
80+
<Accordion title={t(EntityFieldsI18nKey.Resources)} errorIndicator={isSectionInvalid}>
7081
<div className="flex flex-col gap-x-2 gap-y-8">
7182
<CPUFields container={container} setContainer={setContainer} />
7283
<MemoryFields container={container} setContainer={setContainer} />

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerVariables.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useCallback, useMemo } from 'react';
1+
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
22
import { DndProvider } from 'react-dnd';
33
import { HTML5Backend } from 'react-dnd-html5-backend';
44
import { IconPlus } from '@tabler/icons-react';
@@ -8,12 +8,13 @@ import { EnvironmentVariable } from '@/src/models/deployments/variables';
88
import { MOUNT_TYPE, VALUE_TYPE } from '@/src/types/deployments/variables';
99
import { EntityFieldsI18nKey, EnvVariablesI18nKey } from '@/src/constants/i18n';
1010
import { Container } from '@/src/models/deployments/containers';
11-
import { isEditDisabled } from '@/src/utils/deployments/containers';
11+
import { isEditDisabled, isErrorPresent } from '@/src/utils/deployments/containers';
1212
import { useI18n } from '@/src/locales/client';
1313
import { BASE_BUTTON_ICON_PROPS } from '@/src/constants/main-layout';
1414

1515
import Accordion from '@/src/components/Common/Accordion/Accordion';
1616
import Variable from '@/src/components/Deployments/Fields/ContainerVariables/Variable';
17+
import { useSaveValidationContext } from '@/src/context/SaveValidationContext';
1718

1819
interface Props {
1920
container: Container;
@@ -22,6 +23,9 @@ interface Props {
2223

2324
const ContainerVariables: FC<Props> = ({ container, setContainer }) => {
2425
const t = useI18n();
26+
const { errorFields, isValid } = useSaveValidationContext();
27+
28+
const [isSectionInvalid, setSectionInvalid] = useState(false);
2529

2630
const variables = useMemo(() => container.metadata?.envs || [], [container]);
2731

@@ -84,8 +88,16 @@ const ContainerVariables: FC<Props> = ({ container, setContainer }) => {
8488
[findColumn, onChangeVariables, variables],
8589
);
8690

91+
useEffect(() => {
92+
if (!isValid) {
93+
setSectionInvalid(isErrorPresent(errorFields, ['variable_']));
94+
} else {
95+
setSectionInvalid(false);
96+
}
97+
}, [errorFields, isValid]);
98+
8799
return (
88-
<Accordion title={t(EntityFieldsI18nKey.EnvironmentVariables)}>
100+
<Accordion title={t(EntityFieldsI18nKey.EnvironmentVariables)} errorIndicator={isSectionInvalid}>
89101
<div className="flex flex-col gap-y-2">
90102
<DndProvider backend={HTML5Backend}>
91103
<div className="flex flex-col gap-2 lg:pr-2 overflow-auto">

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerVariables/Value.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const ContainerVariableValue: FC<Props> = ({ value, index, onValueChange, mountT
105105
iconBefore={<IconFileArrowRight {...BASE_BUTTON_ICON_PROPS} />}
106106
onClick={handleFileInputClick}
107107
className="absolute right-0"
108+
disabled={disabled}
108109
/>
109110
<input type="file" className="hidden" ref={inputRef} onChange={handleFileUpload} />
110111
</div>

apps/ai-dial-admin/src/components/Deployments/Fields/ContainerVariables/Variable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ const Variable: FC<Props> = ({ index, variable, updateVariable, removeVariable,
173173
</div>
174174
</div>
175175
<div className="w-[40px] flex-shrink-0">
176-
<DialRemoveButton onClick={onRemove} className={index === 0 ? 'mt-3 lg:mt-6' : ''} />
176+
<DialRemoveButton onClick={onRemove} className={index === 0 ? 'mt-3 lg:mt-6' : ''} disabled={disabled} />
177177
</div>
178178
</div>
179179
</DraggableItem>

apps/ai-dial-admin/src/components/EntityHeaderControls/ContainersHeader.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ interface Props extends ContainersButtonsWrapperProps {
1414
tabs: TabModel[];
1515
activeTab: EntityViewTab;
1616
children?: ReactNode;
17-
1817
onChangeActiveTab: (tab: EntityViewTab) => void;
1918
}
2019

0 commit comments

Comments
 (0)