Skip to content

Commit c3f9f8f

Browse files
committed
feat(pci-kubernetes): create clusterconfigfileactions component and add copy related tests
ref: #TAPC-5144 Signed-off-by: Thomas Esseul <[email protected]>
1 parent 5bb51a8 commit c3f9f8f

File tree

5 files changed

+199
-156
lines changed

5 files changed

+199
-156
lines changed

packages/manager/apps/pci-kubernetes/src/components/service/ClusterAccessAndSecurity.component.spec.tsx

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { UseQueryResult } from '@tanstack/react-query';
22
import { render } from '@testing-library/react';
3-
import userEvents from '@testing-library/user-event';
43
import { vi } from 'vitest';
54

6-
import { v6 } from '@ovh-ux/manager-core-api';
7-
85
import * as useKubernetesModule from '@/api/hooks/useKubernetes';
96
import { useRegionInformations } from '@/api/hooks/useRegionInformations';
107
import ClusterAccessAndSecurity from '@/components/service/ClusterAccessAndSecurity.component';
@@ -124,39 +121,6 @@ describe('ClusterAccessAndSecurity', () => {
124121
);
125122
expect(getByText(/kube_service_upgrade_policy_automatic/i)).toBeInTheDocument();
126123
});
127-
128-
it('disables kube config button when status is installing', () => {
129-
const { getByTestId } = render(
130-
<ClusterAccessAndSecurity kubeDetail={{ status: 'INSTALLING' } as TKube} />,
131-
{ wrapper },
132-
);
133-
expect(getByTestId('ClusterAccessAndSecurity-DownloadKubeConfig')).toBeDisabled();
134-
});
135-
136-
it('enables kube config button when status is ready', () => {
137-
const { getByTestId } = render(
138-
<ClusterAccessAndSecurity kubeDetail={{ status: 'READY' } as TKube} />,
139-
{ wrapper },
140-
);
141-
expect(getByTestId('ClusterAccessAndSecurity-DownloadKubeConfig')).not.toBeDisabled();
142-
});
143-
144-
it('enables spinner for kube config button when getting data and button disabled', async () => {
145-
vi.mocked(v6.post).mockReturnValue(new Promise(() => {}));
146-
147-
const { getByTestId } = render(
148-
<ClusterAccessAndSecurity kubeDetail={{ status: 'READY' } as TKube} />,
149-
{ wrapper },
150-
);
151-
152-
await userEvents.click(getByTestId('ClusterAccessAndSecurity-DownloadKubeConfig'));
153-
154-
getByTestId('clusterAccessAndSecurity-spinnerDownloadKubeConfig').click();
155-
156-
expect(getByTestId('clusterAccessAndSecurity-spinnerDownloadKubeConfig')).toBeInTheDocument();
157-
expect(getByTestId('ClusterAccessAndSecurity-DownloadKubeConfig')).toBeDisabled();
158-
});
159-
160124
it('renders nodes URL with clipboard component in 1az', () => {
161125
const { container, getByText } = render(
162126
<ClusterAccessAndSecurity

packages/manager/apps/pci-kubernetes/src/components/service/ClusterAccessAndSecurity.component.tsx

Lines changed: 7 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
22

33
import { useHref } from 'react-router-dom';
44

5-
import { Translation, useTranslation } from 'react-i18next';
5+
import { useTranslation } from 'react-i18next';
66

77
import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core';
88
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
@@ -23,35 +23,17 @@ import {
2323
OsdsText,
2424
OsdsTile,
2525
} from '@ovhcloud/ods-components/react';
26-
import { Button, Icon, Spinner } from '@ovhcloud/ods-react';
2726

28-
import { isApiCustomError } from '@ovh-ux/manager-core-api';
2927
import { useParam } from '@ovh-ux/manager-pci-common';
30-
import {
31-
ActionMenu,
32-
Clipboard,
33-
LinkType,
34-
Links,
35-
useNotifications,
36-
} from '@ovh-ux/manager-react-components';
28+
import { ActionMenu, Clipboard, LinkType, Links } from '@ovh-ux/manager-react-components';
3729

38-
import { useClusterRestrictions, useKubeConfig, useOidcProvider } from '@/api/hooks/useKubernetes';
30+
import { useClusterRestrictions, useOidcProvider } from '@/api/hooks/useKubernetes';
3931
import { useRegionInformations } from '@/api/hooks/useRegionInformations';
40-
import {
41-
CONFIG_FILENAME,
42-
KUBECONFIG_URL,
43-
KUBE_INSTALLING_STATUS,
44-
PROCESSING_STATUS,
45-
} from '@/constants';
46-
import {
47-
downloadContent,
48-
getValidOptionalKeys,
49-
isMultiDeploymentZones,
50-
isOptionalValue,
51-
} from '@/helpers';
52-
import { getClusterUrlFragments } from '@/helpers/matchers/matchers';
32+
import { KUBECONFIG_URL, PROCESSING_STATUS } from '@/constants';
33+
import { getValidOptionalKeys, isMultiDeploymentZones, isOptionalValue } from '@/helpers';
5334
import { TKube } from '@/types';
5435

36+
import { ClusterConfigFileActions } from './ClusterConfigFileActions.component';
5537
import TileLine from './TileLine.component';
5638

5739
export type ClusterAccessAndSecurityProps = {
@@ -64,11 +46,7 @@ export default function ClusterAccessAndSecurity({
6446
const { t } = useTranslation('service');
6547

6648
const { kubeId, projectId } = useParam('projectId', 'kubeId');
67-
const { addError } = useNotifications();
6849
const [isOptional, setIsOptional] = useState(true);
69-
const [ongoingKubeConfigAction, setOngoingKubeConfigAction] = useState<
70-
'copy' | 'download' | null
71-
>(null);
7250

7351
const hrefRestrictions = useHref('../restrictions');
7452
const hrefUAddOIDCProvider = useHref('./add-oidc-provider');
@@ -86,23 +64,6 @@ export default function ClusterAccessAndSecurity({
8664
[oidcProvider],
8765
);
8866

89-
const { mutate: postKubeConfig, isPending: isKubeConfigPending } = useKubeConfig({
90-
projectId,
91-
kubeId,
92-
onError: (error: Error) =>
93-
addError(
94-
<Translation ns="service">
95-
{(_t) =>
96-
_t('kube_service_file_error', {
97-
message: isApiCustomError(error)
98-
? error?.response?.data?.message
99-
: (error?.message ?? null),
100-
})
101-
}
102-
</Translation>,
103-
true,
104-
),
105-
});
10667
const isProcessing = (status: string) => PROCESSING_STATUS.includes(status);
10768
const { data: regionInformations } = useRegionInformations(projectId, kubeDetail?.region);
10869

@@ -112,36 +73,6 @@ export default function ClusterAccessAndSecurity({
11273
);
11374
const hasOptionalValues = validOptionalKeys.length > 0;
11475

115-
const downloadConfigFile = () => {
116-
setOngoingKubeConfigAction('download');
117-
postKubeConfig(undefined, {
118-
onSuccess: (configFile) => {
119-
const clusterShortId = getClusterUrlFragments(kubeDetail.url)?.shortId;
120-
121-
const fileName = clusterShortId
122-
? `${CONFIG_FILENAME}-${clusterShortId}.yml`
123-
: `${CONFIG_FILENAME}.yml`;
124-
125-
downloadContent({
126-
fileContent: configFile.content,
127-
fileName,
128-
});
129-
130-
setOngoingKubeConfigAction(null);
131-
},
132-
});
133-
};
134-
135-
const copyConfigFile = () => {
136-
setOngoingKubeConfigAction('copy');
137-
postKubeConfig(undefined, {
138-
onSuccess: async (configFile) => {
139-
await navigator.clipboard.writeText(configFile.content);
140-
setOngoingKubeConfigAction(null);
141-
},
142-
});
143-
};
144-
14576
const getClusterRestrictionLabel = (length?: number) => {
14677
const suffixes = ['no_count', 'one', 'count'];
14778
const len = length ?? 0;
@@ -230,37 +161,7 @@ export default function ClusterAccessAndSecurity({
230161
/>
231162
</OsdsPopoverContent>
232163
</OsdsPopover>
233-
<div className="flex flex-col items-start gap-3">
234-
<div className="flex items-center">
235-
<Button
236-
size="sm"
237-
variant="ghost"
238-
data-testid="ClusterAccessAndSecurity-CopyKubeConfig"
239-
disabled={isKubeConfigPending || kubeDetail.status === KUBE_INSTALLING_STATUS}
240-
onClick={copyConfigFile}
241-
>
242-
<Icon name="file-copy" /> {t('kube_service_copy_kubeconfig')}
243-
</Button>
244-
{isKubeConfigPending && ongoingKubeConfigAction === 'copy' && (
245-
<Spinner size="sm" data-testid="clusterAccessAndSecurity-spinnerCopyKubeConfig" />
246-
)}
247-
</div>
248-
249-
<div className="flex items-center">
250-
<Button
251-
size="sm"
252-
variant="ghost"
253-
data-testid="ClusterAccessAndSecurity-DownloadKubeConfig"
254-
disabled={isKubeConfigPending || kubeDetail.status === KUBE_INSTALLING_STATUS}
255-
onClick={downloadConfigFile}
256-
>
257-
<Icon name="download" /> {t('kube_service_download_kubeconfig')}
258-
</Button>
259-
{isKubeConfigPending && ongoingKubeConfigAction === 'download' && (
260-
<Spinner size="sm" data-testid="clusterAccessAndSecurity-spinnerDownloadKubeConfig" />
261-
)}
262-
</div>
263-
</div>
164+
<ClusterConfigFileActions projectId={projectId} kubeDetail={kubeDetail} />
264165

265166
<OsdsDivider separator />
266167

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { render } from '@testing-library/react';
2+
import userEvents from '@testing-library/user-event';
3+
4+
import * as KubeAPI from '@/api/data/kubernetes';
5+
import { TKube } from '@/types';
6+
import { wrapper } from '@/wrapperRenders';
7+
8+
import { ClusterConfigFileActions } from './ClusterConfigFileActions.component';
9+
10+
it('disables download kubeconfig button when status is installing', () => {
11+
const { getByTestId } = render(
12+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'INSTALLING' } as TKube} />,
13+
{ wrapper },
14+
);
15+
expect(getByTestId('ClusterConfigFileActions-DownloadKubeConfig')).toBeDisabled();
16+
});
17+
18+
it('enables download kubeconfig button when status is ready', () => {
19+
const { getByTestId } = render(
20+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'READY' } as TKube} />,
21+
{ wrapper },
22+
);
23+
expect(getByTestId('ClusterConfigFileActions-DownloadKubeConfig')).not.toBeDisabled();
24+
});
25+
26+
it('enables spinner for download kubeconfig button when getting data, button disabled and hide copy spinner', async () => {
27+
vi.spyOn(KubeAPI, 'postKubeConfig').mockReturnValue(new Promise(() => {}));
28+
29+
const { getByTestId, queryByTestId } = render(
30+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'READY' } as TKube} />,
31+
{ wrapper },
32+
);
33+
34+
await userEvents.click(getByTestId('ClusterConfigFileActions-DownloadKubeConfig'));
35+
36+
expect(getByTestId('clusterConfigFileActions-spinnerDownloadKubeConfig')).toBeInTheDocument();
37+
expect(getByTestId('ClusterConfigFileActions-DownloadKubeConfig')).toBeDisabled();
38+
expect(queryByTestId('clusterConfigFileActions-spinnerCopyKubeConfig')).not.toBeInTheDocument();
39+
});
40+
41+
it('disables copy kubeconfig button when status is installing', () => {
42+
const { getByTestId } = render(
43+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'INSTALLING' } as TKube} />,
44+
{ wrapper },
45+
);
46+
expect(getByTestId('ClusterConfigFileActions-CopyKubeConfig')).toBeDisabled();
47+
});
48+
49+
it('enables copy kubeconfig button when status is ready', () => {
50+
const { getByTestId } = render(
51+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'READY' } as TKube} />,
52+
{ wrapper },
53+
);
54+
expect(getByTestId('ClusterConfigFileActions-CopyKubeConfig')).not.toBeDisabled();
55+
});
56+
57+
it('enables spinner for copy kubeconfig button when getting data, button disabled and hide download spinner', async () => {
58+
vi.spyOn(KubeAPI, 'postKubeConfig').mockReturnValue(new Promise(() => {}));
59+
60+
const { getByTestId, queryByTestId } = render(
61+
<ClusterConfigFileActions projectId="" kubeDetail={{ status: 'READY' } as TKube} />,
62+
{ wrapper },
63+
);
64+
65+
await userEvents.click(getByTestId('ClusterConfigFileActions-CopyKubeConfig'));
66+
67+
expect(getByTestId('clusterConfigFileActions-spinnerCopyKubeConfig')).toBeInTheDocument();
68+
expect(getByTestId('ClusterConfigFileActions-CopyKubeConfig')).toBeDisabled();
69+
expect(
70+
queryByTestId('clusterConfigFileActions-spinnerDownloadKubeConfig'),
71+
).not.toBeInTheDocument();
72+
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useState } from 'react';
2+
3+
import { Translation, useTranslation } from 'react-i18next';
4+
5+
import { Button, Icon, Spinner } from '@ovhcloud/ods-react';
6+
7+
import { isApiCustomError } from '@ovh-ux/manager-core-api';
8+
import { useNotifications } from '@ovh-ux/manager-react-components';
9+
10+
import { useKubeConfig } from '@/api/hooks/useKubernetes';
11+
import { CONFIG_FILENAME, KUBE_INSTALLING_STATUS } from '@/constants';
12+
import { downloadContent } from '@/helpers';
13+
import { getClusterUrlFragments } from '@/helpers/matchers/matchers';
14+
import { TKube } from '@/types';
15+
16+
export type ClusterConfigFileActionsProps = {
17+
kubeDetail: TKube;
18+
projectId: string;
19+
};
20+
21+
export const ClusterConfigFileActions = ({
22+
kubeDetail,
23+
projectId,
24+
}: ClusterConfigFileActionsProps) => {
25+
const [ongoingKubeConfigAction, setOngoingKubeConfigAction] = useState<
26+
'copy' | 'download' | null
27+
>(null);
28+
29+
const { addError } = useNotifications();
30+
const { t } = useTranslation('service');
31+
32+
const { mutate: postKubeConfig, isPending: isKubeConfigPending } = useKubeConfig({
33+
projectId,
34+
kubeId: kubeDetail.id,
35+
onError: (error: Error) =>
36+
addError(
37+
<Translation ns="service">
38+
{(_t) =>
39+
_t('kube_service_file_error', {
40+
message: isApiCustomError(error)
41+
? error?.response?.data?.message
42+
: (error?.message ?? null),
43+
})
44+
}
45+
</Translation>,
46+
true,
47+
),
48+
});
49+
50+
const downloadConfigFile = () => {
51+
setOngoingKubeConfigAction('download');
52+
postKubeConfig(undefined, {
53+
onSuccess: (configFile) => {
54+
const clusterShortId = getClusterUrlFragments(kubeDetail.url)?.shortId;
55+
56+
const fileName = clusterShortId
57+
? `${CONFIG_FILENAME}-${clusterShortId}.yml`
58+
: `${CONFIG_FILENAME}.yml`;
59+
60+
downloadContent({
61+
fileContent: configFile.content,
62+
fileName,
63+
});
64+
65+
setOngoingKubeConfigAction(null);
66+
},
67+
});
68+
};
69+
70+
const copyConfigFile = () => {
71+
setOngoingKubeConfigAction('copy');
72+
postKubeConfig(undefined, {
73+
onSuccess: async (configFile) => {
74+
await navigator.clipboard.writeText(configFile.content);
75+
setOngoingKubeConfigAction(null);
76+
},
77+
});
78+
};
79+
80+
return (
81+
<div className="flex flex-col items-start gap-3">
82+
<div className="flex items-center">
83+
<Button
84+
size="sm"
85+
variant="ghost"
86+
data-testid="ClusterConfigFileActions-CopyKubeConfig"
87+
disabled={isKubeConfigPending || kubeDetail.status === KUBE_INSTALLING_STATUS}
88+
onClick={copyConfigFile}
89+
>
90+
<Icon name="file-copy" /> {t('kube_service_copy_kubeconfig')}
91+
</Button>
92+
{isKubeConfigPending && ongoingKubeConfigAction === 'copy' && (
93+
<Spinner size="sm" data-testid="clusterConfigFileActions-spinnerCopyKubeConfig" />
94+
)}
95+
</div>
96+
97+
<div className="flex items-center">
98+
<Button
99+
size="sm"
100+
variant="ghost"
101+
data-testid="ClusterConfigFileActions-DownloadKubeConfig"
102+
disabled={isKubeConfigPending || kubeDetail.status === KUBE_INSTALLING_STATUS}
103+
onClick={downloadConfigFile}
104+
>
105+
<Icon name="download" /> {t('kube_service_download_kubeconfig')}
106+
</Button>
107+
{isKubeConfigPending && ongoingKubeConfigAction === 'download' && (
108+
<Spinner size="sm" data-testid="clusterConfigFileActions-spinnerDownloadKubeConfig" />
109+
)}
110+
</div>
111+
</div>
112+
);
113+
};

0 commit comments

Comments
 (0)