Merge pull request #480 from ylei-tsubame/patch-screenshot-icons

Web UI: patch server screenshot representations
main
Digimer 1 year ago committed by GitHub
commit 51319b2389
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      striker-ui-api/out/index.js
  2. 5
      striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts
  3. 1
      striker-ui-api/src/types/ApiServer.d.ts
  4. 93
      striker-ui/components/Display/Preview.tsx
  5. 2
      striker-ui/components/Servers.tsx
  6. 55
      striker-ui/lib/time.ts
  7. 1
      striker-ui/out/_next/static/chunks/227-a3756585a7ef09ae.js
  8. 1
      striker-ui/out/_next/static/chunks/433-a3be905e7a7d3bfc.js
  9. 1
      striker-ui/out/_next/static/chunks/528-72edc50189f30fa9.js
  10. 1
      striker-ui/out/_next/static/chunks/717-8bd60b96d67fd464.js
  11. 1
      striker-ui/out/_next/static/chunks/94-8322ed453a3c08f0.js
  12. 1
      striker-ui/out/_next/static/chunks/94-e103c3735f0e061b.js
  13. 2
      striker-ui/out/_next/static/chunks/pages/anvil-5ff2efa937105177.js
  14. 1
      striker-ui/out/_next/static/chunks/pages/index-03c43a0be65dfb49.js
  15. 1
      striker-ui/out/_next/static/chunks/pages/index-6ff72adab3a682db.js
  16. 1
      striker-ui/out/_next/static/chunks/pages/server-4ac03eba56ccfcbf.js
  17. 1
      striker-ui/out/_next/static/chunks/pages/server-db52258419acacf3.js
  18. 2
      striker-ui/out/_next/static/tsboOH-aG8W5HRHH4dt2_/_buildManifest.js
  19. 0
      striker-ui/out/_next/static/tsboOH-aG8W5HRHH4dt2_/_middlewareManifest.js
  20. 0
      striker-ui/out/_next/static/tsboOH-aG8W5HRHH4dt2_/_ssgManifest.js
  21. 2
      striker-ui/out/anvil.html
  22. 2
      striker-ui/out/config.html
  23. 2
      striker-ui/out/file-manager.html
  24. 2
      striker-ui/out/index.html
  25. 2
      striker-ui/out/init.html
  26. 2
      striker-ui/out/login.html
  27. 2
      striker-ui/out/manage-element.html
  28. 2
      striker-ui/out/server.html
  29. 28
      striker-ui/pages/index.tsx
  30. 7
      striker-ui/pages/server/index.tsx

File diff suppressed because one or more lines are too long

@ -57,7 +57,7 @@ export const getServerDetail: RequestHandler<
}
if (ss) {
const rsBody: ServerDetailScreenshot = { screenshot: '' };
const rsBody: ServerDetailScreenshot = { screenshot: '', timestamp: 0 };
const ssDir = SERVER_PATHS.opt.alteeve.screenshots.self;
let ssNames: string[];
@ -95,13 +95,14 @@ export const getServerDetail: RequestHandler<
stdoutVar(ssMetaLatest, `Latest server screenshot: `);
if (ssMetaLatest) {
const { name } = ssMetaLatest;
const { name, timestamp } = ssMetaLatest;
const ssLatest = readFileSync(path.join(ssDir, name), {
encoding: 'base64',
});
rsBody.screenshot = ssLatest;
rsBody.timestamp = timestamp;
}
return response.send(rsBody);

@ -19,6 +19,7 @@ type ServerDetailParsedQs = {
type ServerDetailScreenshot = {
screenshot: string;
timestamp: number;
};
type ServerDetailVncInfo = {

@ -1,6 +1,6 @@
import {
DesktopWindows as DesktopWindowsIcon,
PowerOffOutlined as PowerOffOutlinedIcon,
PowerSettingsNewOutlined as PowerSettingsNewOutlinedIcon,
} from '@mui/icons-material';
import {
Box,
@ -9,18 +9,25 @@ import {
} from '@mui/material';
import { FC, ReactNode, useEffect, useMemo, useState } from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import { BORDER_RADIUS, GREY } from '../../lib/consts/DEFAULT_THEME';
import {
BORDER_RADIUS,
GREY,
UNSELECTED,
} from '../../lib/consts/DEFAULT_THEME';
import api from '../../lib/api';
import FlexBox from '../FlexBox';
import IconButton, { IconButtonProps } from '../IconButton';
import { InnerPanel, InnerPanelHeader, Panel, PanelHeader } from '../Panels';
import Spinner from '../Spinner';
import { BodyText, HeaderText } from '../Text';
import { elapsed, last, now } from '../../lib/time';
type PreviewOptionalProps = {
externalPreview?: string;
externalTimestamp?: number;
headerEndAdornment?: ReactNode;
isExternalLoading?: boolean;
isExternalPreviewStale?: boolean;
isFetchPreview?: boolean;
isShowControls?: boolean;
@ -28,6 +35,7 @@ type PreviewOptionalProps = {
onClickConnectButton?: IconButtonProps['onClick'];
onClickPreview?: MUIIconButtonProps['onClick'];
serverName?: string;
serverState?: string;
};
type PreviewProps = PreviewOptionalProps & {
@ -39,7 +47,9 @@ const PREVIEW_DEFAULT_PROPS: Required<
> &
Pick<PreviewOptionalProps, 'onClickConnectButton' | 'onClickPreview'> = {
externalPreview: '',
externalTimestamp: 0,
headerEndAdornment: null,
isExternalLoading: false,
isExternalPreviewStale: false,
isFetchPreview: true,
isShowControls: true,
@ -47,6 +57,7 @@ const PREVIEW_DEFAULT_PROPS: Required<
onClickConnectButton: undefined,
onClickPreview: undefined,
serverName: '',
serverState: '',
};
const PreviewPanel: FC<{ isUseInnerPanel: boolean }> = ({
@ -77,27 +88,34 @@ const PreviewPanelHeader: FC<{
const Preview: FC<PreviewProps> = ({
externalPreview = PREVIEW_DEFAULT_PROPS.externalPreview,
externalTimestamp = PREVIEW_DEFAULT_PROPS.externalTimestamp,
headerEndAdornment,
isExternalLoading = PREVIEW_DEFAULT_PROPS.isExternalLoading,
isExternalPreviewStale = PREVIEW_DEFAULT_PROPS.isExternalPreviewStale,
isFetchPreview = PREVIEW_DEFAULT_PROPS.isFetchPreview,
isShowControls = PREVIEW_DEFAULT_PROPS.isShowControls,
isUseInnerPanel = PREVIEW_DEFAULT_PROPS.isUseInnerPanel,
onClickPreview: previewClickHandler,
serverName,
serverState = PREVIEW_DEFAULT_PROPS.serverState,
serverUUID,
onClickConnectButton: connectButtonClickHandle = previewClickHandler,
}) => {
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(true);
const [isPreviewStale, setIsPreviewStale] = useState<boolean>(false);
const [preview, setPreview] = useState<string>('');
const [previewTimstamp, setPreviewTimestamp] = useState<number>(0);
const nao = now();
const previewButtonContent = useMemo(
() =>
preview ? (
serverState === 'running' ? (
<>
<Box
alt=""
component="img"
src={`data:image/png;base64,${preview}`}
src={`data:image;base64,${preview}`}
sx={{
height: '100%',
opacity: isPreviewStale ? '0.4' : '1',
@ -105,46 +123,70 @@ const Preview: FC<PreviewProps> = ({
width: '100%',
}}
/>
{isPreviewStale &&
((sst: number) => {
const { unit, value } = elapsed(nao - sst);
return (
<BodyText position="absolute">
Updated ~{value} {unit} ago
</BodyText>
);
})(previewTimstamp)}
</>
) : (
<PowerOffOutlinedIcon
<PowerSettingsNewOutlinedIcon
sx={{
height: '100%',
width: '100%',
color: UNSELECTED,
height: '80%',
width: '80%',
}}
/>
),
[isPreviewStale, isUseInnerPanel, preview],
[
isPreviewStale,
isUseInnerPanel,
nao,
preview,
previewTimstamp,
serverState,
],
);
useEffect(() => {
if (isFetchPreview) {
(async () => {
try {
const response = await fetch(
`${API_BASE_URL}/server/${serverUUID}?ss=1`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
const { screenshot: fetchedScreenshot } = await response.json();
const { data } = await api.get<{
screenshot: string;
timestamp: number;
}>(`/server/${serverUUID}?ss=1`);
setPreview(fetchedScreenshot);
setIsPreviewStale(false);
const { screenshot, timestamp } = data;
setPreview(screenshot);
setPreviewTimestamp(timestamp);
setIsPreviewStale(!last(timestamp, 300));
} catch {
setIsPreviewStale(true);
} finally {
setIsPreviewLoading(false);
}
})();
} else if (externalPreview) {
} else if (!isExternalLoading) {
setPreview(externalPreview);
setPreviewTimestamp(externalTimestamp);
setIsPreviewStale(isExternalPreviewStale);
setIsPreviewLoading(false);
}
}, [externalPreview, isExternalPreviewStale, isFetchPreview, serverUUID]);
}, [
externalPreview,
externalTimestamp,
isExternalLoading,
isExternalPreviewStale,
isFetchPreview,
serverUUID,
]);
return (
<PreviewPanel isUseInnerPanel={isUseInnerPanel}>
@ -153,12 +195,13 @@ const Preview: FC<PreviewProps> = ({
</PreviewPanelHeader>
<FlexBox row sx={{ '& > :first-child': { flexGrow: 1 } }}>
{/* Box wrapper below is required to keep external preview size sane. */}
<Box>
<Box textAlign="center">
{isPreviewLoading ? (
<Spinner mt="1em" mb="1em" />
) : (
<MUIIconButton
component="span"
disabled={!preview}
onClick={previewClickHandler}
sx={{
borderRadius: BORDER_RADIUS,
@ -170,7 +213,7 @@ const Preview: FC<PreviewProps> = ({
</MUIIconButton>
)}
</Box>
{isShowControls && (
{isShowControls && preview && (
<FlexBox>
<IconButton onClick={connectButtonClickHandle}>
<DesktopWindowsIcon />

@ -335,7 +335,7 @@ const Servers = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => {
className={classes.button}
key={server.serverUUID}
component={showCheckbox ? 'div' : 'a'}
href={`/server?uuid=${server.serverUUID}&server_name=${server.serverName}`}
href={`/server?uuid=${server.serverUUID}&server_name=${server.serverName}&server_state=${server.serverState}`}
onClick={() => handleChange(server.serverUUID)}
>
<Box display="flex" flexDirection="row" width="100%">

@ -0,0 +1,55 @@
const before = (time: number, limit: number): boolean => {
const diff = time - limit;
return diff > 0;
};
const now = (ms?: boolean): number => {
let nao = Date.now();
if (!ms) nao = Math.floor(nao / 1000);
return nao;
};
const last = (
time: number,
duration: number,
{ ms }: { ms?: boolean } = {},
): boolean => {
const diff = now(ms) - time;
return diff <= duration;
};
const elapsed = (
duration: number,
): { h: number; m: number; s: number; unit: string; value: number } => {
let src = duration;
const parts = [60, 60].reduce<number[]>((previous, multiplier) => {
const remainder = src % multiplier;
previous.push(remainder);
src = (src - remainder) / multiplier;
return previous;
}, []);
const [s, m, h] = [...parts, src];
const significant = [
{ unit: 'h', value: h },
{ unit: 'm', value: m },
].find(({ value }) => value) ?? { unit: 's', value: s };
return {
h,
m,
s,
...significant,
};
};
export { before, elapsed, last, now };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
self.__BUILD_MANIFEST=function(s,c,a,t,e,n,i,d,f,b,u,k,h,j,r,g,l,_){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,a,t,d,f,h,"static/chunks/717-8bd60b96d67fd464.js",c,e,n,i,j,r,"static/chunks/pages/index-03c43a0be65dfb49.js"],"/_error":["static/chunks/pages/_error-2280fa386d040b66.js"],"/anvil":[s,a,t,d,f,h,c,e,n,i,j,"static/chunks/pages/anvil-5058ba8058633c3d.js"],"/config":[s,a,t,u,"static/chunks/586-4e70511cf6d7632f.js",c,e,n,i,b,k,g,"static/chunks/pages/config-0cb597caf390573f.js"],"/file-manager":["static/chunks/29107295-fbcfe2172188e46f.js",s,a,t,d,"static/chunks/176-7308c25ba374961e.js",c,e,i,b,"static/chunks/pages/file-manager-1ae01a78e266275a.js"],"/init":[s,a,d,f,u,l,c,e,n,i,_,"static/chunks/pages/init-053607258b5d7d64.js"],"/login":[s,a,t,c,e,n,b,k,"static/chunks/pages/login-1b987b077ffc3420.js"],"/manage-element":[s,a,t,d,f,u,l,"static/chunks/111-2605129c170ed35d.js",c,e,n,i,b,k,_,g,"static/chunks/pages/manage-element-6b42a013966413d3.js"],"/server":[s,t,"static/chunks/227-a3756585a7ef09ae.js",c,r,"static/chunks/pages/server-db52258419acacf3.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/manage-element","/server"]}}("static/chunks/382-f51344f6f9208507.js","static/chunks/62-532ed713980da8db.js","static/chunks/438-0147a63d98e89439.js","static/chunks/894-e57948de523bcf96.js","static/chunks/195-fa06e61dd4339031.js","static/chunks/987-1ff0d82724b0e58b.js","static/chunks/157-d1418743accab385.js","static/chunks/182-08683bbe95fbb010.js","static/chunks/434-07ec1dcc649bdd0c.js","static/chunks/248-749f2bec4cb43d28.js","static/chunks/644-c7c6e21c71345aed.js","static/chunks/336-8a7866afcf131f68.js","static/chunks/485-77798bccc4308d0e.js","static/chunks/825-0b3ee47570192a02.js","static/chunks/94-e103c3735f0e061b.js","static/chunks/560-0ed707609765e23a.js","static/chunks/676-6159ce853338cc1f.js","static/chunks/86-447b52c8195dea3d.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
self.__BUILD_MANIFEST=function(s,c,a,t,e,n,i,f,d,b,u,k,h,j,r,g,l,_){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,a,t,f,d,h,"static/chunks/433-a3be905e7a7d3bfc.js",c,e,n,i,j,r,"static/chunks/pages/index-6ff72adab3a682db.js"],"/_error":["static/chunks/pages/_error-2280fa386d040b66.js"],"/anvil":[s,a,t,f,d,h,c,e,n,i,j,"static/chunks/pages/anvil-5ff2efa937105177.js"],"/config":[s,a,t,u,"static/chunks/586-4e70511cf6d7632f.js",c,e,n,i,b,k,g,"static/chunks/pages/config-0cb597caf390573f.js"],"/file-manager":["static/chunks/29107295-fbcfe2172188e46f.js",s,a,t,f,"static/chunks/176-7308c25ba374961e.js",c,e,i,b,"static/chunks/pages/file-manager-1ae01a78e266275a.js"],"/init":[s,a,f,d,u,l,c,e,n,i,_,"static/chunks/pages/init-053607258b5d7d64.js"],"/login":[s,a,t,c,e,n,b,k,"static/chunks/pages/login-1b987b077ffc3420.js"],"/manage-element":[s,a,t,f,d,u,l,"static/chunks/111-2605129c170ed35d.js",c,e,n,i,b,k,_,g,"static/chunks/pages/manage-element-6b42a013966413d3.js"],"/server":[s,t,"static/chunks/528-72edc50189f30fa9.js",c,r,"static/chunks/pages/server-4ac03eba56ccfcbf.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/manage-element","/server"]}}("static/chunks/382-f51344f6f9208507.js","static/chunks/62-532ed713980da8db.js","static/chunks/438-0147a63d98e89439.js","static/chunks/894-e57948de523bcf96.js","static/chunks/195-fa06e61dd4339031.js","static/chunks/987-1ff0d82724b0e58b.js","static/chunks/157-d1418743accab385.js","static/chunks/182-08683bbe95fbb010.js","static/chunks/434-07ec1dcc649bdd0c.js","static/chunks/248-749f2bec4cb43d28.js","static/chunks/644-c7c6e21c71345aed.js","static/chunks/336-8a7866afcf131f68.js","static/chunks/485-77798bccc4308d0e.js","static/chunks/825-0b3ee47570192a02.js","static/chunks/94-8322ed453a3c08f0.js","static/chunks/560-0ed707609765e23a.js","static/chunks/676-6159ce853338cc1f.js","static/chunks/86-447b52c8195dea3d.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -17,10 +17,13 @@ import { Panel, PanelHeader } from '../components/Panels';
import periodicFetch from '../lib/fetchers/periodicFetch';
import ProvisionServerDialog from '../components/ProvisionServerDialog';
import Spinner from '../components/Spinner';
import { last } from '../lib/time';
type ServerListItem = ServerOverviewMetadata & {
isScreenshotStale?: boolean;
loading?: boolean;
screenshot: string;
timestamp: number;
};
const createServerPreviewContainer = (
@ -47,15 +50,19 @@ const createServerPreviewContainer = (
anvilName,
anvilUUID,
isScreenshotStale,
loading,
screenshot,
serverName,
serverState,
serverUUID,
timestamp,
}) => (
<Preview
externalPreview={screenshot}
externalTimestamp={timestamp}
headerEndAdornment={[
<Link
href={`/server?uuid=${serverUUID}&server_name=${serverName}`}
href={`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}`}
key={`server_list_to_server_${serverUUID}`}
>
{serverName}
@ -70,6 +77,7 @@ const createServerPreviewContainer = (
{anvilName}
</Link>,
]}
isExternalLoading={loading}
isExternalPreviewStale={isScreenshotStale}
isFetchPreview={false}
isShowControls={false}
@ -77,9 +85,10 @@ const createServerPreviewContainer = (
key={`server-preview-${serverUUID}`}
onClickPreview={() => {
router.push(
`/server?uuid=${serverUUID}&server_name=${serverName}&vnc=1`,
`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`,
);
}}
serverState={serverState}
serverUUID={serverUUID}
/>
),
@ -150,15 +159,21 @@ const Dashboard: FC = () => {
?.screenshot || '';
const item: ServerListItem = {
...serverOverview,
loading: true,
screenshot: previousScreenshot,
timestamp: 0,
};
fetchJSON<{ screenshot: string }>(
fetchJSON<{ screenshot: string; timestamp: number }>(
`${API_BASE_URL}/server/${serverUUID}?ss=1`,
)
.then(({ screenshot }) => {
.then(({ screenshot, timestamp }) => {
if (screenshot.length === 0) return;
item.isScreenshotStale = !last(timestamp, 300);
item.loading = false;
item.screenshot = screenshot;
item.isScreenshotStale = false;
item.timestamp = timestamp;
const allServersWithScreenshots = [...serverListItems];
@ -174,6 +189,9 @@ const Dashboard: FC = () => {
})
.catch(() => {
item.isScreenshotStale = true;
})
.finally(() => {
item.loading = false;
});
return item;

@ -34,10 +34,12 @@ const Server = (): JSX.Element => {
const [previewMode, setPreviewMode] = useState<boolean>(true);
const router = useRouter();
const { server_name, uuid, vnc } = router.query;
const { server_name, server_state, uuid, vnc } = router.query;
const isConnectVNC: boolean = (vnc?.toString() || '').length > 0;
const serverUUID: string = uuid?.toString() || '';
const serverName: string = server_name?.toString() || '';
const serverState: string = server_state?.toString() || '';
const serverUUID: string = uuid?.toString() || '';
useEffect(() => {
if (isConnectVNC) {
@ -58,6 +60,7 @@ const Server = (): JSX.Element => {
setPreviewMode(false);
}}
serverName={serverName}
serverState={serverState}
serverUUID={serverUUID}
/>
</Box>

Loading…
Cancel
Save