Merge pull request #548 from ylei-tsubame/issues/526-reload-on-add

Web UI: patch 452, 493, 508, and 526
main
Digimer 1 year ago committed by GitHub
commit 26bfb80707
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts
  2. 55
      striker-ui/components/Display/FullSize.tsx
  3. 70
      striker-ui/components/Display/Preview.tsx
  4. 30
      striker-ui/components/ManageFence/ManageFencePanel.tsx
  5. 97
      striker-ui/components/ManageManifest/ManageManifestPanel.tsx
  6. 4
      striker-ui/hooks/useChecklist.tsx
  7. 7
      striker-ui/hooks/useFormUtils.ts
  8. 17
      striker-ui/pages/index.tsx
  9. 11
      striker-ui/types/FormUtils.d.ts

@ -43,8 +43,8 @@ export const getAnvilStore: RequestHandler<
b.storage_group_name,
d.scan_lvm_vg_size,
d.scan_lvm_vg_free,
SUM(d.scan_lvm_vg_size) AS total_vg_size,
SUM(d.scan_lvm_vg_free) AS total_vg_free
MIN(d.scan_lvm_vg_size) AS total_vg_size,
MIN(d.scan_lvm_vg_free) AS total_vg_free
FROM anvils AS a
JOIN storage_groups AS b
ON a.anvil_uuid = b.storage_group_anvil_uuid

@ -1,5 +1,6 @@
import {
Close as CloseIcon,
Dashboard as DashboardIcon,
Keyboard as KeyboardIcon,
} from '@mui/icons-material';
import { Box, Menu, styled, Typography } from '@mui/material';
@ -100,17 +101,18 @@ const FullSize: FC<FullSizeProps> = ({
}, [serverUUID]);
const disconnectServerVnc = useCallback(() => {
if (rfb?.current) {
rfb.current.disconnect();
rfb.current = null;
}
setRfbConnectArgs(undefined);
}, []);
const reconnectServerVnc = useCallback(() => {
if (!rfb?.current) return;
rfb.current.disconnect();
rfb.current = null;
disconnectServerVnc();
connectServerVnc();
}, [connectServerVnc]);
}, [connectServerVnc, disconnectServerVnc]);
const updateVncReconnectTimer = useCallback((): void => {
const intervalId = setInterval((): void => {
@ -175,28 +177,49 @@ const FullSize: FC<FullSizeProps> = ({
const vncDisconnectElement = useMemo(
() => (
<IconButton
onClick={(...args) => {
disconnectServerVnc();
onClickCloseButton?.call(null, ...args);
}}
variant="redcontained"
>
<CloseIcon />
</IconButton>
<Box>
<IconButton
onClick={(...args) => {
disconnectServerVnc();
onClickCloseButton?.call(null, ...args);
}}
>
<CloseIcon />
</IconButton>
</Box>
),
[disconnectServerVnc, onClickCloseButton],
);
const returnHomeElement = useMemo(
() => (
<Box>
<IconButton
onClick={() => {
if (!window) return;
disconnectServerVnc();
window.location.assign('/');
}}
>
<DashboardIcon />
</IconButton>
</Box>
),
[disconnectServerVnc],
);
const vncToolbarElement = useMemo(
() =>
showScreen && (
<>
{keyboardMenuElement}
{returnHomeElement}
{vncDisconnectElement}
</>
),
[keyboardMenuElement, showScreen, vncDisconnectElement],
[keyboardMenuElement, returnHomeElement, showScreen, vncDisconnectElement],
);
useEffect(() => {

@ -27,6 +27,7 @@ type PreviewOptionalProps = {
externalPreview?: string;
externalTimestamp?: number;
headerEndAdornment?: ReactNode;
hrefPreview?: string;
isExternalLoading?: boolean;
isExternalPreviewStale?: boolean;
isFetchPreview?: boolean;
@ -43,12 +44,19 @@ type PreviewProps = PreviewOptionalProps & {
};
const PREVIEW_DEFAULT_PROPS: Required<
Omit<PreviewOptionalProps, 'onClickConnectButton' | 'onClickPreview'>
Omit<
PreviewOptionalProps,
'hrefPreview' | 'onClickConnectButton' | 'onClickPreview'
>
> &
Pick<PreviewOptionalProps, 'onClickConnectButton' | 'onClickPreview'> = {
Pick<
PreviewOptionalProps,
'hrefPreview' | 'onClickConnectButton' | 'onClickPreview'
> = {
externalPreview: '',
externalTimestamp: 0,
headerEndAdornment: null,
hrefPreview: undefined,
isExternalLoading: false,
isExternalPreviewStale: false,
isFetchPreview: true,
@ -90,6 +98,7 @@ const Preview: FC<PreviewProps> = ({
externalPreview = PREVIEW_DEFAULT_PROPS.externalPreview,
externalTimestamp = PREVIEW_DEFAULT_PROPS.externalTimestamp,
headerEndAdornment,
hrefPreview,
isExternalLoading = PREVIEW_DEFAULT_PROPS.isExternalLoading,
isExternalPreviewStale = PREVIEW_DEFAULT_PROPS.isExternalPreviewStale,
isFetchPreview = PREVIEW_DEFAULT_PROPS.isFetchPreview,
@ -153,6 +162,44 @@ const Preview: FC<PreviewProps> = ({
],
);
const iconButton = useMemo(() => {
if (isPreviewLoading) {
return <Spinner mb="1em" mt="1em" />;
}
const disabled = !preview;
const sx: MUIIconButtonProps['sx'] = {
borderRadius: BORDER_RADIUS,
color: GREY,
padding: 0,
};
if (hrefPreview) {
return (
<MUIIconButton disabled={disabled} href={hrefPreview} sx={sx}>
{previewButtonContent}
</MUIIconButton>
);
}
return (
<MUIIconButton
component="span"
disabled={disabled}
onClick={previewClickHandler}
sx={sx}
>
{previewButtonContent}
</MUIIconButton>
);
}, [
hrefPreview,
isPreviewLoading,
preview,
previewButtonContent,
previewClickHandler,
]);
useEffect(() => {
if (isFetchPreview) {
(async () => {
@ -195,24 +242,7 @@ const Preview: FC<PreviewProps> = ({
</PreviewPanelHeader>
<FlexBox row sx={{ '& > :first-child': { flexGrow: 1 } }}>
{/* Box wrapper below is required to keep external preview size sane. */}
<Box textAlign="center">
{isPreviewLoading ? (
<Spinner mt="1em" mb="1em" />
) : (
<MUIIconButton
component="span"
disabled={!preview}
onClick={previewClickHandler}
sx={{
borderRadius: BORDER_RADIUS,
color: GREY,
padding: 0,
}}
>
{previewButtonContent}
</MUIIconButton>
)}
</Box>
<Box textAlign="center">{iconButton}</Box>
{isShowControls && preview && (
<FlexBox>
<IconButton onClick={connectButtonClickHandle}>

@ -130,6 +130,9 @@ const ManageFencePanel: FC = () => {
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
const [formDialogProps, setFormDialogProps] = useConfirmDialogProps();
const [fenceOverviews, setFenceOverviews] = useProtectedState<
APIFenceOverview | undefined
>(undefined);
const [fenceTemplate, setFenceTemplate] = useProtectedState<
APIFenceTemplate | undefined
>(undefined);
@ -137,16 +140,29 @@ const ManageFencePanel: FC = () => {
const [isLoadingFenceTemplate, setIsLoadingFenceTemplate] =
useProtectedState<boolean>(true);
const { data: fenceOverviews, isLoading: isFenceOverviewsLoading } =
const { isLoading: isFenceOverviewsLoading } =
periodicFetch<APIFenceOverview>(`${API_BASE_URL}/fence`, {
onSuccess: (data) => setFenceOverviews(data),
refreshInterval: 60000,
});
const getFenceOverviews = useCallback(() => {
api.get('/fence').then(({ data }) => {
setFenceOverviews(data);
});
}, [setFenceOverviews]);
const formUtils = useFormUtils([INPUT_ID_FENCE_AGENT], messageGroupRef);
const { isFormInvalid, isFormSubmitting, submitForm } = formUtils;
const { buildDeleteDialogProps, checks, getCheck, hasChecks, setCheck } =
useChecklist({ list: fenceOverviews });
const {
buildDeleteDialogProps,
checks,
getCheck,
hasChecks,
resetChecklist,
setCheck,
} = useChecklist({ list: fenceOverviews });
const getFormSummaryEntryLabel = useCallback<GetFormEntryLabelFunction>(
({ cap, depth, key }) => (depth === 0 ? cap(key) : key),
@ -195,6 +211,7 @@ const ManageFencePanel: FC = () => {
<>Failed to add fence device. {parentMsg}</>
),
method: 'post',
onSuccess: () => getFenceOverviews(),
successMsg: `Added fence device ${name}`,
url: '/fence',
});
@ -227,6 +244,10 @@ const ManageFencePanel: FC = () => {
<>Failed to delete fence device(s). {parentMsg}</>
),
method: 'delete',
onSuccess: () => {
getFenceOverviews();
resetChecklist();
},
url: '/fence',
});
},
@ -284,6 +305,7 @@ const ManageFencePanel: FC = () => {
<>Failed to update fence device. {parentMsg}</>
),
method: 'put',
onSuccess: () => getFenceOverviews(),
successMsg: `Updated fence device ${fenceName}`,
url: `/fence/${fenceUUID}`,
});
@ -356,9 +378,11 @@ const ManageFencePanel: FC = () => {
fenceTemplate,
formUtils,
getCheck,
getFenceOverviews,
getFormSummaryEntryLabel,
hasChecks,
isEditFences,
resetChecklist,
setCheck,
setConfirmDialogProps,
setFormDialogProps,

@ -1,4 +1,4 @@
import { FC, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
@ -47,8 +47,6 @@ import useFormUtils from '../../hooks/useFormUtils';
import useIsFirstRender from '../../hooks/useIsFirstRender';
import useProtectedState from '../../hooks/useProtectedState';
const MSG_ID_MANIFEST_API = 'api';
const getFormData = (
...[{ target }]: DivFormEventHandlerParameters
): APIBuildManifestRequestBody => {
@ -165,8 +163,9 @@ const ManageManifestPanel: FC = () => {
useProtectedState<boolean>(true);
const [isLoadingManifestTemplate, setIsLoadingManifestTemplate] =
useProtectedState<boolean>(true);
const [isSubmittingForm, setIsSubmittingForm] =
useProtectedState<boolean>(false);
const [manifestOverviews, setManifestOverviews] = useProtectedState<
APIManifestOverviewList | undefined
>(undefined);
const [manifestDetail, setManifestDetail] = useProtectedState<
APIManifestDetail | undefined
>(undefined);
@ -174,11 +173,18 @@ const ManageManifestPanel: FC = () => {
APIManifestTemplate | undefined
>(undefined);
const { data: manifestOverviews, isLoading: isLoadingManifestOverviews } =
const { isLoading: isLoadingManifestOverviews } =
periodicFetch<APIManifestOverviewList>(`${API_BASE_URL}/manifest`, {
onSuccess: (data) => setManifestOverviews(data),
refreshInterval: 60000,
});
const getManifestOverviews = useCallback(() => {
api.get('/manifest').then(({ data }) => {
setManifestOverviews(data);
});
}, [setManifestOverviews]);
const formUtils = useFormUtils(
[
INPUT_ID_AI_DOMAIN,
@ -190,7 +196,7 @@ const ManageManifestPanel: FC = () => {
],
messageGroupRef,
);
const { isFormInvalid, setMessage } = formUtils;
const { isFormInvalid, isFormSubmitting, submitForm } = formUtils;
const runFormUtils = useFormUtils(
[
@ -200,12 +206,22 @@ const ManageManifestPanel: FC = () => {
],
messageGroupRef,
);
const { isFormInvalid: isRunFormInvalid } = runFormUtils;
const {
isFormInvalid: isRunFormInvalid,
isFormSubmitting: isRunFormSubmitting,
submitForm: submitRunForm,
} = runFormUtils;
const { buildDeleteDialogProps, checks, getCheck, hasChecks, setCheck } =
useChecklist({
list: manifestOverviews,
});
const {
buildDeleteDialogProps,
checks,
getCheck,
hasChecks,
resetChecklist,
setCheck,
} = useChecklist({
list: manifestOverviews,
});
const {
hostConfig: { hosts: mdetailHosts = {} } = {},
@ -226,43 +242,6 @@ const ManageManifestPanel: FC = () => {
[manifestTemplate],
);
const submitForm = useCallback(
({
body,
getErrorMsg,
method,
successMsg,
url,
}: {
body: Record<string, unknown>;
getErrorMsg: (parentMsg: ReactNode) => ReactNode;
method: 'delete' | 'post' | 'put';
successMsg?: ReactNode;
url: string;
}) => {
setIsSubmittingForm(true);
api
.request({ data: body, method, url })
.then(() => {
setMessage(MSG_ID_MANIFEST_API, {
children: successMsg,
type: 'info',
});
})
.catch((apiError) => {
const emsg = handleAPIError(apiError);
emsg.children = getErrorMsg(emsg.children);
setMessage(MSG_ID_MANIFEST_API, emsg);
})
.finally(() => {
setIsSubmittingForm(false);
});
},
[setIsSubmittingForm, setMessage],
);
const addManifestFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Add',
@ -291,6 +270,7 @@ const ManageManifestPanel: FC = () => {
<>Failed to add install manifest. {parentMsg}</>
),
method: 'post',
onSuccess: () => getManifestOverviews(),
successMsg: 'Successfully added install manifest',
url: '/manifest',
});
@ -304,6 +284,7 @@ const ManageManifestPanel: FC = () => {
}),
[
formUtils,
getManifestOverviews,
knownFences,
knownUpses,
mtemplateDomain,
@ -338,6 +319,7 @@ const ManageManifestPanel: FC = () => {
<>Failed to update install manifest. {parentMsg}</>
),
method: 'put',
onSuccess: () => getManifestOverviews(),
successMsg: `Successfully updated install manifest ${mdetailName}`,
url: `/manifest/${mdetailUuid}`,
});
@ -360,6 +342,7 @@ const ManageManifestPanel: FC = () => {
setConfirmDialogProps,
submitForm,
mdetailUuid,
getManifestOverviews,
],
);
@ -383,7 +366,7 @@ const ManageManifestPanel: FC = () => {
actionProceedText: 'Run',
content: <FormSummary entries={body} hasPassword />,
onProceedAppend: () => {
submitForm({
submitRunForm({
body,
getErrorMsg: (parentMsg) => (
<>Failed to run install manifest. {parentMsg}</>
@ -410,7 +393,7 @@ const ManageManifestPanel: FC = () => {
mdetailName,
mdetailHosts,
setConfirmDialogProps,
submitForm,
submitRunForm,
mdetailUuid,
],
);
@ -460,6 +443,10 @@ const ManageManifestPanel: FC = () => {
<>Delete manifest(s) failed. {parentMsg}</>
),
method: 'delete',
onSuccess: () => {
getManifestOverviews();
resetChecklist();
},
url: `/manifest`,
});
},
@ -512,9 +499,11 @@ const ManageManifestPanel: FC = () => {
checks,
getCheck,
getManifestDetail,
getManifestOverviews,
hasChecks,
isEditManifests,
manifestOverviews,
resetChecklist,
setCheck,
setConfirmDialogProps,
setManifestDetail,
@ -587,7 +576,7 @@ const ManageManifestPanel: FC = () => {
<FormDialog
{...addManifestFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isSubmittingForm}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={addManifestFormDialogRef}
scrollContent
@ -596,7 +585,7 @@ const ManageManifestPanel: FC = () => {
<FormDialog
{...editManifestFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isSubmittingForm}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={editManifestFormDialogRef}
scrollContent
@ -605,7 +594,7 @@ const ManageManifestPanel: FC = () => {
<FormDialog
{...runManifestFormDialogProps}
disableProceed={isRunFormInvalid}
loadingAction={isSubmittingForm}
loadingAction={isRunFormSubmitting}
preActionArea={messageArea}
ref={runManifestFormDialogRef}
scrollContent

@ -16,6 +16,7 @@ const useChecklist = ({
hasAllChecks: boolean;
hasChecks: boolean;
multipleItems: boolean;
resetChecklist: () => void;
setAllChecks: SetAllChecksFunction;
setCheck: SetCheckFunction;
} => {
@ -61,6 +62,8 @@ const useChecklist = ({
[checklist],
);
const resetChecklist = useCallback(() => setChecklist({}), []);
const setAllChecks = useCallback<SetAllChecksFunction>(
(checked) =>
setChecklist(
@ -89,6 +92,7 @@ const useChecklist = ({
hasAllChecks,
hasChecks,
multipleItems,
resetChecklist,
setAllChecks,
setCheck,
};

@ -20,6 +20,12 @@ const useFormUtils = <
const [formSubmitting, setFormSubmitting] = useProtectedState<boolean>(false);
const [formValidity, setFormValidity] = useState<FormValidity<M>>({});
const setApiMessage = useCallback(
(message?: Message) =>
messageGroupRef?.current?.setMessage?.call(null, 'api', message),
[messageGroupRef],
);
const setMessage = useCallback(
(key: keyof M, message?: Message) => {
messageGroupRef?.current?.setMessage?.call(null, String(key), message);
@ -136,6 +142,7 @@ const useFormUtils = <
formValidity,
isFormInvalid: formInvalid,
isFormSubmitting: formSubmitting,
setApiMessage,
setFormValidity,
setMessage,
setMessageRe,

@ -1,7 +1,6 @@
import { Add as AddIcon } from '@mui/icons-material';
import { Box, Divider, Grid } from '@mui/material';
import Head from 'next/head';
import { NextRouter, useRouter } from 'next/router';
import { FC, useEffect, useRef, useState } from 'react';
import API_BASE_URL from '../lib/consts/API_BASE_URL';
@ -28,10 +27,7 @@ type ServerListItem = ServerOverviewMetadata & {
timestamp: number;
};
const createServerPreviewContainer = (
servers: ServerListItem[],
router: NextRouter,
) => (
const createServerPreviewContainer = (servers: ServerListItem[]) => (
<Grid
alignContent="stretch"
columns={{ xs: 1, sm: 2, md: 3, xl: 4 }}
@ -84,16 +80,12 @@ const createServerPreviewContainer = (
{anvilName}
</Link>,
]}
hrefPreview={`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`}
isExternalLoading={loading}
isExternalPreviewStale={isScreenshotStale}
isFetchPreview={false}
isShowControls={false}
isUseInnerPanel
onClickPreview={() => {
router.push(
`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`,
);
}}
serverState={serverState}
serverUUID={serverUUID}
/>
@ -129,7 +121,6 @@ const filterServers = (allServers: ServerListItem[], searchTerm: string) =>
const Dashboard: FC = () => {
const componentMountedRef = useRef(true);
const router = useRouter();
const [allServers, setAllServers] = useState<ServerListItem[]>([]);
const [excludeServers, setExcludeServers] = useState<ServerListItem[]>([]);
@ -244,11 +235,11 @@ const Dashboard: FC = () => {
value={inputSearchTerm}
/>
</PanelHeader>
{createServerPreviewContainer(includeServers, router)}
{createServerPreviewContainer(includeServers)}
{includeServers.length > 0 && (
<Divider sx={{ backgroundColor: DIVIDER }} />
)}
{createServerPreviewContainer(excludeServers, router)}
{createServerPreviewContainer(excludeServers)}
</>
)}
</Panel>

@ -41,17 +41,12 @@ type FormUtils<M extends MapToInputTestID> = {
formValidity: FormValidity<M>;
isFormInvalid: boolean;
isFormSubmitting: boolean;
setApiMessage: (message?: Message) => void;
setFormValidity: import('react').Dispatch<
import('react').SetStateAction<FormValidity<M>>
>;
setMessage: (
key: keyof M,
message?: import('../components/MessageBox').Message,
) => void;
setMessageRe: (
re: RegExp,
message?: import('../components/MessageBox').Message,
) => void;
setMessage: (key: keyof M, message?: Message) => void;
setMessageRe: (re: RegExp, message?: Message) => void;
setValidity: (key: keyof M, value?: boolean) => void;
setValidityRe: (re: RegExp, value?: boolean) => void;
submitForm: SubmitFormFunction;

Loading…
Cancel
Save