Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
Merge pull request #45 from CharVstack/CVS-142-フロントエンドからvmの操作
Browse files Browse the repository at this point in the history
CVS-142-フロントエンドからvmの操作
  • Loading branch information
Explosive6363 committed Dec 13, 2022
2 parents 0f13f4c + ff17b20 commit aeafa86
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 55 deletions.
14 changes: 2 additions & 12 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import tsConfigPaths from 'vite-tsconfig-paths';
import { UserConfig } from 'vitest/config';
import { StorybookConfig, CoreConfig, Options } from '@storybook/core-common';
import { Weaken } from 'utilitypes';
import { StorybookViteConfig } from '@storybook/builder-vite';

interface CustomizedCoreConfig extends Weaken<CoreConfig, 'builder'> {
builder: CoreConfig['builder'] | 'storybook-builder-vite';
}
interface CustomizedStorybookConfig extends Weaken<StorybookConfig, 'core'> {
core: CustomizedCoreConfig;
viteFinal?: (config: UserConfig, options: Options) => UserConfig;
}

const config: CustomizedStorybookConfig = {
const config: StorybookViteConfig = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-controls',
Expand Down
18 changes: 10 additions & 8 deletions .storybook/preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { mswDecorator, initialize } from 'msw-storybook-addon';

const prefix = import.meta.env.VITE_STORYBOOK_PREFIX;

initialize({
serviceWorker:
prefix !== undefined
? {
url: `/${prefix}mockServiceWorker.js`,
}
: {},
});
if (import.meta.env.MODE !== 'test') {
initialize({
serviceWorker:
prefix !== undefined
? {
url: `/${prefix}mockServiceWorker.js`,
}
: {},
});
}

export const decorators = [
(Story) => {
Expand Down
9 changes: 1 addition & 8 deletions src/components/organisms/Buttons/CreateNewVmButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* --------------------------------------
* VM作成ダイアログのために一時的に作成
* ToDo: Instanceページ作成時に削除予定
* --------------------------------------
*/

import { Button } from '@mui/material';

import { useWriteOnlyCreateVmDialog } from '@components/organisms/Dialogs';
Expand All @@ -16,7 +9,7 @@ export const CreateNewVmButton = () => {
};

return (
<Button variant="contained" onClick={handleClickOpen}>
<Button variant="contained" sx={{ ml: 1, mb: 1 }} onClick={handleClickOpen}>
新規仮想マシンの作成
</Button>
);
Expand Down
12 changes: 6 additions & 6 deletions src/components/organisms/Forms/CreateVm/CreateVmForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export const Default: ComponentStoryObj<typeof CreateVmForm> = {
},
play: ({ canvasElement }) => {
const canvas = within(canvasElement);
const name: HTMLInputElement = canvas.getByLabelText('名前');
const cpu: HTMLInputElement = canvas.getByLabelText('CPU');
const memory: HTMLInputElement = canvas.getByLabelText('メモリ');
const name = canvas.getByLabelText<HTMLInputElement>('名前');
const cpu = canvas.getByLabelText<HTMLInputElement>('CPU');
const memory = canvas.getByLabelText<HTMLInputElement>('メモリ');
userEvent.clear(name);
userEvent.clear(cpu);
userEvent.clear(memory);
Expand All @@ -52,9 +52,9 @@ export const InConfirm: ComponentStoryObj<typeof CreateVmForm> = {
},
play: ({ canvasElement }) => {
const canvas = within(canvasElement);
const name: HTMLInputElement = canvas.getByLabelText('名前');
const cpu: HTMLInputElement = canvas.getByLabelText('CPU');
const memory: HTMLInputElement = canvas.getByLabelText('メモリ');
const name = canvas.getByLabelText<HTMLInputElement>('名前');
const cpu = canvas.getByLabelText<HTMLInputElement>('CPU');
const memory = canvas.getByLabelText<HTMLInputElement>('メモリ');
userEvent.type(name, 'foo');
userEvent.type(cpu, '1');
userEvent.type(memory, '1');
Expand Down
24 changes: 24 additions & 0 deletions src/components/organisms/Menu/VmControlMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { within } from '@storybook/testing-library';
import { composeStories } from '@storybook/testing-react';
import { render } from '@testing-library/react';

import * as stories from './VmControlMenu.stories';

describe('VmControlMenu', () => {
describe('Interaction', () => {
const { Default, Disabled } = composeStories(stories);

test('ボタンが有効', () => {
const { container } = render(<Default />);
const canvas = within(container);
const button = canvas.getByText<HTMLButtonElement>('Actions');
expect(button.disabled).toBeFalsy();
});
test('ボタンが無効', () => {
const { container } = render(<Disabled />);
const canvas = within(container);
const button = canvas.getByText<HTMLButtonElement>('Actions');
expect(button.disabled).toBeTruthy();
});
});
});
24 changes: 24 additions & 0 deletions src/components/organisms/Menu/VmControlMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentMeta, ComponentStoryObj } from '@storybook/react';

import schema from '@openapi-spec/v1.json';

import { BaseVmControlMenu as VmControlMenu } from './VmControlMenu';

export default {
component: VmControlMenu,
} as ComponentMeta<typeof VmControlMenu>;

export const Default: ComponentStoryObj<typeof VmControlMenu> = {
args: {
vms: [
schema.components.responses.GetAllVMsList200Response.content['application/json'].examples['example-1'].value
.vms[0].metadata.id,
],
},
};

export const Disabled: ComponentStoryObj<typeof VmControlMenu> = {
args: {
vms: [],
},
};
81 changes: 81 additions & 0 deletions src/components/organisms/Menu/VmControlMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { getSWRDefaultKey } from '@aspida/swr';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import { Box } from '@mui/material';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useState, useCallback } from 'react';
import { useSWRConfig } from 'swr';

import { useSelectedVmReadOnlyAtom } from '@components/organisms/Tables';
import { apiClient } from '@lib/apiClient';

type BaseVmControlMenuProps = {
vms: string[];
};

export const VmControlMenu = () => {
const vms = useSelectedVmReadOnlyAtom();

return <BaseVmControlMenu vms={vms} />;
};

export const BaseVmControlMenu = ({ vms }: BaseVmControlMenuProps) => {
const { mutate } = useSWRConfig();
const handleSubmit = useCallback(
(action: 'start' | 'shutdown' | 'reboot' | 'reset') => async () => {
await Promise.all(
vms.map(async (vm) => {
await apiClient.api.v1.vms
._vmId(vm)
.power.$post({ body: { action } })
.then(async () => {
await mutate(getSWRDefaultKey(apiClient.api.v1.vms._vmId(vm)));
handleClose();
})
.catch(() => {
handleClose();
});
})
);
},
[vms, mutate]
);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

return (
<Box sx={{ ml: 1, mb: 1 }}>
<Button
disabled={vms.length === 0}
variant="contained"
onClick={handleClick}
color="success"
endIcon={<KeyboardArrowDownIcon />}
>
<PowerSettingsNewIcon />
Actions
</Button>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<Box sx={{ width: 180 }}>
<MenuItem onClick={handleSubmit('start')} disableRipple>
Start
</MenuItem>
<MenuItem onClick={handleSubmit('shutdown')} disableRipple>
Shutdown
</MenuItem>
<MenuItem onClick={handleSubmit('reboot')} disableRipple>
Restart
</MenuItem>
</Box>
</Menu>
</Box>
);
};
15 changes: 15 additions & 0 deletions src/components/organisms/Tables/InstanceTable/InstanceTable.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import useAspidaSWR from '@aspida/swr';
import { useTheme } from '@mui/material';
import { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid';
import { atom, useSetAtom, useAtomValue } from 'jotai';
import { useState } from 'react';

import { Vm } from '@api-hooks/v1/@types';
import { LoadingSpinner } from '@components/molecules/Progress';
import { StatusColumn } from '@components/organisms/Columns';
import { apiClient } from '@lib/apiClient';

const baseAtom = atom<string[]>([]);

const selectedVmAtom = atom<string[], string[]>(
(get) => get(baseAtom),
(_get, set, newValue) => set(baseAtom, newValue)
);

export const useSelectedVmReadOnlyAtom = () => useAtomValue(selectedVmAtom);
export const useSelectedVmWriteOnlyAtom = () => useSetAtom(selectedVmAtom);

const columns: GridColDef[] = [
{ field: 'name', headerName: 'VM', minWidth: 288, flex: 1 },
{
Expand All @@ -21,6 +32,7 @@ const columns: GridColDef[] = [
export const InstanceTable = () => {
const { data } = useAspidaSWR(apiClient.api.v1.vms);
const [pageSize, setPageSize] = useState(10);
const setSelectedVm = useSelectedVmWriteOnlyAtom();

const {
palette: {
Expand All @@ -43,6 +55,9 @@ export const InstanceTable = () => {
rowsPerPageOptions={[10, 25, 50]}
checkboxSelection
autoHeight
onSelectionModelChange={(selected) => {
setSelectedVm(selected.map((v) => v.toString()));
}}
/>
);
};
63 changes: 45 additions & 18 deletions src/lib/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,54 @@ export const HANDLERS = {
}),
},
GetVMByVMId: {
success: restGet(apiClient.api.v1.vms._vmId('2a4316ce-7351-4d4f-9f2d-b5f523e688ab'), (_, res, ctx) => {
const example = schema.components.responses.GetVMByVMId200Response.content['application/json'].examples[
'example-1'
].value as GetVMByVMId200Response;
return res(ctx.json(example));
}),
success: restGet(
apiClient.api.v1.vms._vmId(
schema.components.responses.GetAllVMsList200Response.content['application/json'].examples['example-1'].value
.vms[0].metadata.id
),
(_, res, ctx) => {
const example = schema.components.responses.GetVMByVMId200Response.content['application/json'].examples[
'example-1'
].value as GetVMByVMId200Response;
return res(ctx.json(example));
}
),
},
GetVMPowerByVMId: {
success: restGet(apiClient.api.v1.vms._vmId('2a4316ce-7351-4d4f-9f2d-b5f523e688ab').power, (_, res, ctx) => {
const example = schema.components.responses.GetVMPowerByVMId200Response.content['application/json'].examples[
'example-1'
].value as GetVMPowerByVMId200Response;
return res(ctx.json(example));
}),
success: restGet(
apiClient.api.v1.vms._vmId(
schema.components.responses.GetAllVMsList200Response.content['application/json'].examples['example-1'].value
.vms[0].metadata.id
).power,
(_, res, ctx) => {
const example = schema.components.responses.GetVMPowerByVMId200Response.content['application/json'].examples[
'example-1'
].value as GetVMPowerByVMId200Response;
return res(ctx.json(example));
}
),
},
PatchUpdateVMByVMId200Response: {
success: restPatch(apiClient.api.v1.vms._vmId('2a4316ce-7351-4d4f-9f2d-b5f523e688ab'), (_, res, ctx) => {
const example = schema.components.responses.PatchUpdateVMByVMId200Response.content['application/json'].examples[
'example-1'
].value as PatchUpdateVMByVMId200Response;
return res(ctx.json(example));
}),
success: restPatch(
apiClient.api.v1.vms._vmId(
schema.components.responses.GetAllVMsList200Response.content['application/json'].examples['example-1'].value
.vms[0].metadata.id
),
(_, res, ctx) => {
const example = schema.components.responses.PatchUpdateVMByVMId200Response.content['application/json'].examples[
'example-1'
].value as PatchUpdateVMByVMId200Response;
return res(ctx.json(example));
}
),
},
PostVMPowerActionByVMIdResponse: {
success: restPost(
apiClient.api.v1.vms._vmId(
schema.components.responses.GetAllVMsList200Response.content['application/json'].examples['example-1'].value
.vms[0].metadata.id
).power,
(_, res, ctx) => res(ctx.status(204))
),
},
};
8 changes: 6 additions & 2 deletions src/pages/VMs/VMs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Container } from '@mui/material';
import { Container, Stack } from '@mui/material';

import { CreateNewVmButton } from '@components/organisms/Buttons';
import { CreateVmDialog } from '@components/organisms/Dialogs';
import { VmControlMenu } from '@components/organisms/Menu/VmControlMenu';
import { InstanceTable } from '@components/organisms/Tables';
import { DashBoard } from '@templates/DashBoard';

export const VMs = () => (
<DashBoard>
<CreateNewVmButton />
<Container>
<Stack direction="row" spacing={1} justifyContent="end">
<CreateNewVmButton />
<VmControlMenu />
</Stack>
<InstanceTable />
</Container>
<CreateVmDialog />
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@*": ["./src/*"]
},
"baseUrl": "./",
"types": ["vitest/globals"]
"types": ["vitest/globals", "vitest/importMeta"]
},
"include": ["src", "vitest.setup.ts"]
}
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh';
import jotaiDebugLabel from 'jotai/babel/plugin-debug-label';

export default defineConfig({
define: {
'import.meta.vitest': false,
},
plugins: [react({ babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] } }), tsConfigPaths()],
build: {
outDir: 'build',
Expand Down

0 comments on commit aeafa86

Please sign in to comment.