Skip to content

Commit

Permalink
feat(pci-instances): add start & stop actions
Browse files Browse the repository at this point in the history
ref: TAPC-2149 TAPC-2150
Signed-off-by: Frédéric Vilcot <[email protected]>
  • Loading branch information
fredericvilcot committed Nov 15, 2024
1 parent a63e560 commit 2590465
Show file tree
Hide file tree
Showing 24 changed files with 542 additions and 247 deletions.
12 changes: 6 additions & 6 deletions packages/manager/apps/pci-instances/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
"type:check": "tsc --noEmit"
},
"dependencies": {
"@ovh-ux/manager-config": "^7.5.3-alpha.0",
"@ovh-ux/manager-core-api": "^0.9.0-alpha.0",
"@ovh-ux/manager-pci-common": "^0.8.0-alpha.6",
"@ovh-ux/manager-react-components": "^1.41.0-alpha.4",
"@ovh-ux/manager-react-core-application": "^0.10.8-alpha.0",
"@ovh-ux/manager-react-shell-client": "^0.8.0-alpha.0",
"@ovh-ux/manager-config": "^8.0.2",
"@ovh-ux/manager-core-api": "^0.9.0",
"@ovh-ux/manager-pci-common": "^0.8.1",
"@ovh-ux/manager-react-components": "^1.41.1",
"@ovh-ux/manager-react-core-application": "^0.11.1",
"@ovh-ux/manager-react-shell-client": "^0.8.1",
"@ovh-ux/manager-tailwind-config": "^0.2.0",
"@ovhcloud/ods-common-core": "17.2.2",
"@ovhcloud/ods-common-stencil": "17.2.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"pci_instances_actions_delete_instance_title": "Supprimer une instance",
"pci_instances_actions_delete_instance_confirmation_message": "Êtes-vous sûr de vouloir supprimer l'instance {{ name }} ?",
"pci_instances_actions_delete_instance_error_message": "Une erreur est survenue lors de la suppression de l'instance {{ name }}.",
"pci_instances_actions_delete_instance_success_message": "L'instance {{ name }} a bien été supprimée.",
"pci_instances_actions_stop_instance_title": "Arrêt de votre instance",
"pci_instances_actions_stop_instance_confirmation_message": "Vous allez arrêter votre instance {{ name }}. Les ressources dédiées à votre instance Public Cloud sont toujours réservées (adresse IP incluse). Vous pouvez redémarrer votre instance à tout moment. Dans l'intervalle, vous êtes toujours facturé au même prix pour votre instance.",
"pci_instances_actions_stop_instance_error_message": "Une erreur est survenue lors de l'arrêt de l'instance {{ name }}.",
"pci_instances_actions_stop_instance_success_message": "L'instance {{ name }} a été arrêtée.",
"pci_instances_actions_start_instance_title": "Démarrage de votre instance",
"pci_instances_actions_start_instance_confirmation_message": "Vous allez démarrer votre instance {{ name }}.",
"pci_instances_actions_start_instance_error_message": "Une erreur est survenue lors du démarrage de l'instance {{ name }}.",
"pci_instances_actions_start_instance_success_message": "L'instance {{ name }} a été démarrée.",
"pci_instances_actions_instance_unknown_error_message": "Une erreur est survenue. Notre équipe technique est informée et travaille à résoudre le problème."
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
"pci_instances_list_unknown_error_message2": "Nos équipes techniques ont été notifiées de ce problème et travaillent à sa résolution.",
"pci_instances_list_action_instance_details": "Détails de l'instance",
"pci_instances_list_action_delete_instance": "Supprimer",
"pci_instances_list_action_autobackup": "Créer une sauvegarde automatisée"
"pci_instances_list_action_autobackup": "Créer une sauvegarde automatisée",
"pci_instances_list_action_start_instance": "Démarrer",
"pci_instances_list_action_stop_instance": "Arrêter"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,89 @@ import {
ODS_ICON_SIZE,
ODS_BUTTON_SIZE,
ODS_TEXT_LEVEL,
ODS_DIVIDER_SIZE,
} from '@ovhcloud/ods-components';
import {
OsdsButton,
OsdsDivider,
OsdsIcon,
OsdsMenu,
OsdsMenuItem,
OsdsText,
} from '@ovhcloud/ods-components/react';
import { FC } from 'react';
import { FC, useMemo } from 'react';
import { DeepReadonly } from '@/types/utils.type';

export type TActionsMenuItem = {
export type TActionsMenuItem = DeepReadonly<{
label: string;
href?: string;
group?: string;
onMenuItemClick?: () => void;
};
}>;
export type TActionsMenuProps = {
items: TActionsMenuItem[];
};

export const ActionsMenu: FC<TActionsMenuProps> = ({ items }) => (
<OsdsMenu>
<OsdsButton
data-testid="actions-menu-button"
slot={'menu-title'}
circle
color={ODS_THEME_COLOR_INTENT.primary}
variant={ODS_BUTTON_VARIANT.stroked}
>
<OsdsIcon
name={ODS_ICON_NAME.ELLIPSIS}
export const groupActionMenuItems = (items: TActionsMenuItem[]) =>
items.reduce<Record<string, TActionsMenuItem[]>>(
(acc, { group, ...rest }) => {
if (!group) {
const others: TActionsMenuItem[] = acc.others || [];
return { ...acc, others: [...others, rest] };
}
return { ...acc, [group]: [...(acc[group] || []), rest] };
},
{},
);

export const ActionsMenu: FC<TActionsMenuProps> = ({ items }) => {
const groupedItems = useMemo(() => groupActionMenuItems(items), [items]);

return (
<OsdsMenu>
<OsdsButton
data-testid="actions-menu-button"
slot={'menu-title'}
circle
color={ODS_THEME_COLOR_INTENT.primary}
size={ODS_ICON_SIZE.xxs}
/>
</OsdsButton>
{items.map(({ label, href, onMenuItemClick }) => (
<OsdsMenuItem key={label}>
<OsdsButton
size={ODS_BUTTON_SIZE.sm}
variant={ODS_BUTTON_VARIANT.ghost}
variant={ODS_BUTTON_VARIANT.stroked}
>
<OsdsIcon
name={ODS_ICON_NAME.ELLIPSIS}
color={ODS_THEME_COLOR_INTENT.primary}
data-testid="actions-menu-item"
{...(href && { href })}
{...(onMenuItemClick && { onClick: onMenuItemClick })}
>
<OsdsText
size={ODS_THEME_TYPOGRAPHY_SIZE._500}
level={ODS_TEXT_LEVEL.button}
color={ODS_THEME_COLOR_INTENT.primary}
slot={'start'}
>
{label}
</OsdsText>
</OsdsButton>
</OsdsMenuItem>
))}
</OsdsMenu>
);
size={ODS_ICON_SIZE.xxs}
/>
</OsdsButton>
{Object.values(groupedItems).map((value, index, arr) => (
<div key={index}>
{value.map(({ label, href, onMenuItemClick }) => (
<OsdsMenuItem key={label}>
<OsdsButton
size={ODS_BUTTON_SIZE.sm}
variant={ODS_BUTTON_VARIANT.ghost}
color={ODS_THEME_COLOR_INTENT.primary}
data-testid="actions-menu-item"
{...(href && { href })}
{...(onMenuItemClick && { onClick: onMenuItemClick })}
>
<OsdsText
size={ODS_THEME_TYPOGRAPHY_SIZE._500}
level={ODS_TEXT_LEVEL.button}
color={ODS_THEME_COLOR_INTENT.primary}
slot={'start'}
>
{label}
</OsdsText>
</OsdsButton>
</OsdsMenuItem>
))}
{arr.length - 1 !== index && (
<div className="px-4">
<OsdsDivider separator size={ODS_DIVIDER_SIZE.one} />
</div>
)}
</div>
))}
</OsdsMenu>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, test, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { ActionsMenu, TActionsMenuItem } from './ActionsMenu.component';
import {
ActionsMenu,
groupActionMenuItems,
TActionsMenuItem,
} from './ActionsMenu.component';

const onMenuItemClickMock = vi.fn();

Expand All @@ -19,6 +23,25 @@ const renderActionsMenu = (items: TActionsMenuItem[]) => {
render(<ActionsMenu items={items} />);
};

describe('Considering the groupActionMenuItems() function', () => {
test("should group items by 'group' property", () => {
const items: TActionsMenuItem[] = [
{ group: 'boot', label: 'Start' },
{ group: 'boot', label: 'Stop' },
{ group: 'delete', label: 'Delete' },
{ label: 'Foo' },
];

const result = groupActionMenuItems(items);

expect(result).toEqual({
boot: [{ label: 'Start' }, { label: 'Stop' }],
delete: [{ label: 'Delete' }],
others: [{ label: 'Foo' }],
});
});
});

describe('Considering the ActionsMenu component', () => {
test('Should render only action menu button with Icon as first child if items prop is []', () => {
renderActionsMenu([]);
Expand Down
17 changes: 16 additions & 1 deletion packages/manager/apps/pci-instances/src/data/api/instance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { v6 } from '@ovh-ux/manager-core-api';
import {
TDeleteInstanceDto,
TInstanceDto,
TRetrieveInstancesQueryParams,
TStartInstanceDto,
TStopInstanceDto,
} from '@/types/instance/api.types';

export const getInstances = (
Expand Down Expand Up @@ -31,5 +34,17 @@ export const getInstances = (
export const deleteInstance = (
projectId: string,
instanceId: string,
): Promise<null> =>
): Promise<TDeleteInstanceDto> =>
v6.delete(`/cloud/project/${projectId}/instance/${instanceId}`);

export const stopInstance = (
projectId: string,
instanceId: string,
): Promise<TStopInstanceDto> =>
v6.post(`/cloud/project/${projectId}/instance/${instanceId}/stop`);

export const startInstance = (
projectId: string,
instanceId: string,
): Promise<TStartInstanceDto> =>
v6.post(`/cloud/project/${projectId}/instance/${instanceId}/start`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useMutation } from '@tanstack/react-query';
import { useCallback } from 'react';
import {
deleteInstance,
startInstance,
stopInstance,
} from '@/data/api/instance';
import { DeepReadonly } from '@/types/utils.type';
import { instancesQueryKey } from '@/utils';
import {
TDeleteInstanceDto,
TStopInstanceDto,
TStartInstanceDto,
} from '@/types/instance/api.types';

export type TMutationFnType = 'delete' | 'start' | 'stop';
export type TMutationFnReturnType =
| TDeleteInstanceDto
| TStopInstanceDto
| TStartInstanceDto;
export type TMutationFnVariables = string | null;

export type TUseInstanceActionCallbacks = DeepReadonly<{
onSuccess?: (data?: TMutationFnReturnType) => void;
onError?: (error: unknown) => void;
}>;

export const useInstanceAction = (
type: TMutationFnType | null,
projectId: string,
{ onError, onSuccess }: TUseInstanceActionCallbacks = {},
) => {
const mutationKey = instancesQueryKey(projectId, [
'instance',
...(type !== null ? [type] : []),
]);
const mutationFn = useCallback(
(instanceId: string | null) => {
if (!instanceId) return Promise.reject();
switch (type) {
case 'delete':
return deleteInstance(projectId, instanceId);
case 'start':
return startInstance(projectId, instanceId);
case 'stop':
return stopInstance(projectId, instanceId);
default:
return Promise.reject();
}
},
[projectId, type],
);

const mutation = useMutation<
TMutationFnReturnType,
unknown,
TMutationFnVariables,
unknown
>({
mutationKey,
mutationFn,
onError,
onSuccess,
});

return {
mutationHandler: mutation.mutate,
...mutation,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
TInstance,
updateDeletedInstanceStatus,
useInstances,
} from './useInstances';
} from '../useInstances';
import { setupInstancesServer } from '@/__mocks__/instance/node';
import { useDeleteInstance } from './useDeleteInstance';
import { TInstanceDto } from '@/types/instance/api.types';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useMutation } from '@tanstack/react-query';
import {
UseMutateFunction,
useMutation,
UseMutationResult,
} from '@tanstack/react-query';
import { deleteInstance } from '@/data/api/instance';
import { DeepReadonly } from '@/types/utils.type';
import { instancesQueryKey } from '@/utils';
Expand Down Expand Up @@ -28,3 +32,15 @@ export const useDeleteInstance = (
...mutation,
};
};

export const instanceMutationFn = (type: 'delete', mutationArgs: any): any => {
switch (type) {
case 'delete':
return useDeleteInstance(mutationArgs.projectId, {
onError: mutationArgs.onError,
onSuccess: mutationArgs.onSuccess,
});
default:
return null;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useQueryClient,
} from '@tanstack/react-query';
import { useCallback, useEffect, useMemo } from 'react';
import { FilterWithLabel } from '@ovh-ux/manager-react-components/src/components/filters/interface';
import { FilterWithLabel } from '@ovh-ux/manager-react-components';
import { getInstances } from '@/data/api/instance';
import { instancesQueryKey } from '@/utils';
import { DeepReadonly } from '@/types/utils.type';
Expand Down Expand Up @@ -69,6 +69,25 @@ export const updateDeletedInstanceStatus = (
);
};

export const getInstanceNameById = (
id: string | null,
queryClient: QueryClient,
): string | undefined => {
if (!id) return undefined;

const data = queryClient.getQueriesData<InfiniteData<TInstanceDto[], number>>(
{
predicate: (query: Query) => query.queryKey.includes('list'),
},
);
return data.reduce((acc, [, result]) => {
if (acc.length) return acc;
if (result)
return result.pages.flat().find((elt) => elt.id === id)?.name ?? acc;
return acc;
}, '');
};

const buildInstanceStatusSeverity = (
status: TInstanceStatusDto,
): TInstanceStatusSeverity => {
Expand Down
Loading

0 comments on commit 2590465

Please sign in to comment.