forked from github-starred/komodo
get home screen working pretty much
This commit is contained in:
@@ -1,18 +1,107 @@
|
||||
import { Component } from "solid-js";
|
||||
import { Component, createMemo, createSignal, For, Match, Switch } from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { combineClasses } from "../../util/helpers";
|
||||
import Input from "../shared/Input";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Tabs from "../shared/tabs/Tabs";
|
||||
import s from "./home.module.scss"
|
||||
import Summary from "./Summary";
|
||||
import AddServer from "./Tree/AddServer";
|
||||
import Builds from "./Tree/Builds";
|
||||
import Server from "./Tree/Server";
|
||||
import Updates from "./Updates/Updates";
|
||||
|
||||
const Home: Component<{}> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
const { width } = useAppDimensions();
|
||||
const [serverFilter, setServerFilter] = createSignal("");
|
||||
const serverIDs = createMemo(() => {
|
||||
if (servers.loaded()) {
|
||||
const filters = serverFilter()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0)
|
||||
.map((term) => term.toLowerCase());
|
||||
return servers.ids()?.filter((id) => {
|
||||
const name = servers.get(id)!.server.name;
|
||||
for (const term of filters) {
|
||||
if (!name.includes(term)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Summary />
|
||||
</div>
|
||||
<div>
|
||||
<Updates />
|
||||
</div>
|
||||
</>
|
||||
<Switch>
|
||||
<Match when={width() >= 1200}>
|
||||
<Grid class={combineClasses(s.Home)}>
|
||||
<Tabs
|
||||
localStorageKey="home-tab"
|
||||
containerClass={s.Tabs}
|
||||
tabs={[
|
||||
{
|
||||
title: "deployments",
|
||||
element: () => (
|
||||
<Grid gap="0.5rem">
|
||||
<Input
|
||||
placeholder="filter servers"
|
||||
value={serverFilter()}
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
|
||||
<AddServer />
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "builds",
|
||||
element: () => <Builds />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Grid>
|
||||
<Summary />
|
||||
<Updates />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Match>
|
||||
<Match when={width() < 1200}>
|
||||
<Grid class={s.Home}>
|
||||
<Summary />
|
||||
<Tabs
|
||||
localStorageKey="home-tab"
|
||||
containerClass={s.Tabs}
|
||||
tabs={[
|
||||
{
|
||||
title: "deployments",
|
||||
element: () => (
|
||||
<Grid gap="0.5rem">
|
||||
<Input
|
||||
placeholder="filter servers"
|
||||
value={serverFilter()}
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
|
||||
<AddServer />
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "builds",
|
||||
element: () => <Builds />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Updates />
|
||||
</Grid>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
65
frontend/src/components/home/Tree/AddServer.tsx
Normal file
65
frontend/src/components/home/Tree/AddServer.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Component, onMount } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { client, pushNotification } from "../../..";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { CreateServerBody } from "../../../util/client_types";
|
||||
import { useToggle } from "../../../util/hooks";
|
||||
import Input from "../../shared/Input";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import CenterMenu from "../../shared/menu/CenterMenu";
|
||||
|
||||
const AddServer: Component<{}> = () => {
|
||||
const [show, toggleShow] = useToggle();
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title="add server"
|
||||
target="add server"
|
||||
targetClass="green shadow"
|
||||
targetStyle={{ width: "100%" }}
|
||||
content={<Content close={toggleShow} />}
|
||||
position="center"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Content: Component<{ close: () => void }> = (p) => {
|
||||
const { ws } = useAppState();
|
||||
let nameInput: HTMLInputElement | undefined;
|
||||
const [server, setServer] = createStore<CreateServerBody>({
|
||||
name: "",
|
||||
address: "",
|
||||
});
|
||||
onMount(() => nameInput?.focus());
|
||||
const create = async () => {
|
||||
if (server.name.length > 0 && server.address.length > 0) {
|
||||
await client.create_server(server);
|
||||
p.close();
|
||||
} else {
|
||||
pushNotification("bad", "a field is empty. fill in all fields");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Grid placeItems="center" style={{ padding: "2rem 1rem 1rem 1rem" }}>
|
||||
<Input
|
||||
ref={nameInput}
|
||||
value={server.name}
|
||||
onEdit={(name) => setServer("name", name)}
|
||||
placeholder="name"
|
||||
style={{ "font-size": "1.5rem" }}
|
||||
/>
|
||||
<Input
|
||||
value={server.address}
|
||||
onEdit={(address) => setServer("address", address)}
|
||||
placeholder="address"
|
||||
style={{ "font-size": "1.5rem" }}
|
||||
/>
|
||||
<button class="green" style={{ width: "100%" }} onClick={create}>
|
||||
add
|
||||
</button>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddServer;
|
||||
49
frontend/src/components/home/Tree/Builds.tsx
Normal file
49
frontend/src/components/home/Tree/Builds.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../../types";
|
||||
import { combineClasses, getId } from "../../../util/helpers";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import HoverMenu from "../../shared/menu/HoverMenu";
|
||||
import s from "../home.module.scss";
|
||||
import { NewBuild } from "./New";
|
||||
|
||||
const Builds: Component<{}> = (p) => {
|
||||
const { builds } = useAppState();
|
||||
return (
|
||||
<Grid class={combineClasses(s.Deployments)}>
|
||||
<For each={builds.ids()}>{(id) => <Build id={id} />}</For>
|
||||
{/* <NewBuild /> */}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const Build: Component<{ id: string }> = (p) => {
|
||||
const { builds } = useAppState();
|
||||
const { user } = useUser();
|
||||
const build = () => builds.get(p.id)!;
|
||||
const permissionLevel = () => {
|
||||
const level = build().permissions[getId(user())];
|
||||
return level ? level : PermissionLevel.None;
|
||||
};
|
||||
return (
|
||||
<Show when={build()}>
|
||||
<A href={`/build/${p.id}`} class={combineClasses(s.DropdownItem)}>
|
||||
<div>{build().name}</div>
|
||||
<Show
|
||||
when={!user().admin && permissionLevel() !== PermissionLevel.None}
|
||||
>
|
||||
<HoverMenu
|
||||
target={<Icon type="edit" style={{ padding: "0.25rem" }} />}
|
||||
content="you are a collaborator"
|
||||
padding="0.5rem"
|
||||
position="bottom right"
|
||||
/>
|
||||
</Show>
|
||||
</A>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
export default Builds;
|
||||
54
frontend/src/components/home/Tree/Deployment.tsx
Normal file
54
frontend/src/components/home/Tree/Deployment.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../../types";
|
||||
import { combineClasses, deploymentStateClass, getId } from "../../../util/helpers";
|
||||
import Circle from "../../shared/Circle";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import HoverMenu from "../../shared/menu/HoverMenu";
|
||||
import s from "../home.module.scss";
|
||||
|
||||
const Deployment: Component<{ id: string }> = (p) => {
|
||||
const { deployments } = useAppState();
|
||||
const { user } = useUser();
|
||||
const deployment = () => deployments.get(p.id)!;
|
||||
const permissionLevel = () => {
|
||||
const level = deployment().deployment.permissions![getId(user())];
|
||||
return level ? level : PermissionLevel.None;
|
||||
};
|
||||
return (
|
||||
<Show when={deployment()}>
|
||||
<A
|
||||
href={`/deployment/${p.id}`}
|
||||
class={combineClasses(
|
||||
s.DropdownItem,
|
||||
)}
|
||||
>
|
||||
<h2>{deployment().deployment.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!user().admin && permissionLevel() !== PermissionLevel.None
|
||||
}
|
||||
>
|
||||
<HoverMenu
|
||||
target={<Icon type="edit" style={{ padding: "0.25rem" }} />}
|
||||
content="you are a collaborator"
|
||||
padding="0.5rem"
|
||||
position="bottom center"
|
||||
/>
|
||||
</Show>
|
||||
<div style={{ opacity: 0.7 }}>{deployments.status(p.id)}</div>
|
||||
<Circle
|
||||
size={1}
|
||||
class={deploymentStateClass(deployments.state(p.id))}
|
||||
/>
|
||||
</Flex>
|
||||
</A>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Deployment;
|
||||
96
frontend/src/components/home/Tree/New.tsx
Normal file
96
frontend/src/components/home/Tree/New.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Component, createSignal, onMount, Show } from "solid-js";
|
||||
import { client, pushNotification } from "../../..";
|
||||
import { useKeyDown, useToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Input from "../../shared/Input";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
|
||||
export const NewDeployment: Component<{ serverID: string }> = (p) => {
|
||||
const [showNew, toggleShowNew] = useToggle();
|
||||
const create = (name: string) => {
|
||||
client.create_deployment({ name, server_id: p.serverID });
|
||||
};
|
||||
return (
|
||||
<Show
|
||||
when={showNew()}
|
||||
fallback={
|
||||
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<New
|
||||
create={create}
|
||||
close={toggleShowNew}
|
||||
placeholder="name deployment"
|
||||
/>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewBuild: Component<{ serverID: string }> = (p) => {
|
||||
const [showNew, toggleShowNew] = useToggle();
|
||||
const create = (name: string) => {
|
||||
client.create_build({ name, server_id: p.serverID });
|
||||
};
|
||||
return (
|
||||
<Show
|
||||
when={showNew()}
|
||||
fallback={
|
||||
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<New placeholder="name build" create={create} close={toggleShowNew} />
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
const New: Component<{
|
||||
create: (value: string) => void;
|
||||
close: () => void;
|
||||
placeholder: string;
|
||||
}> = (p) => {
|
||||
const [name, setName] = createSignal("");
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
onMount(() => {
|
||||
inputRef?.focus();
|
||||
});
|
||||
useKeyDown("Escape", p.close);
|
||||
const create = () => {
|
||||
if (name().length > 0) {
|
||||
p.create(name());
|
||||
setName("");
|
||||
p.close();
|
||||
} else {
|
||||
pushNotification("bad", "please provide a name");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex justifyContent="space-between">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={p.placeholder}
|
||||
value={name()}
|
||||
onEdit={setName}
|
||||
onEnter={create}
|
||||
style={{ width: "20rem" }}
|
||||
/>
|
||||
<Flex gap="0.4rem">
|
||||
<button class="green" onClick={create}>
|
||||
create
|
||||
</button>
|
||||
{/* <ConfirmButton
|
||||
color="green"
|
||||
onConfirm={create}
|
||||
>
|
||||
create
|
||||
</ConfirmButton> */}
|
||||
<button class="red" onClick={p.close}>
|
||||
<Icon type="cross" />
|
||||
</button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
145
frontend/src/components/home/Tree/Server.tsx
Normal file
145
frontend/src/components/home/Tree/Server.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Component, createMemo, createSignal, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { combineClasses, getId } from "../../../util/helpers";
|
||||
import { useLocalStorageToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import Deployment from "./Deployment";
|
||||
import s from "../home.module.scss";
|
||||
import { NewDeployment } from "./New";
|
||||
import Loading from "../../shared/loading/Loading";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { PermissionLevel, ServerStatus } from "../../../types";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
// import StatGraphs from "../../server/StatGraphs/StatGraphs";
|
||||
|
||||
const Server: Component<{ id: string }> = (p) => {
|
||||
const { servers, serverStats, deployments } = useAppState();
|
||||
const { width } = useAppDimensions();
|
||||
const { user } = useUser();
|
||||
const navigate = useNavigate();
|
||||
const [open, toggleOpen] = useLocalStorageToggle(p.id + "-homeopen");
|
||||
const server = () => servers.get(p.id);
|
||||
const deploymentIDs = createMemo(() => {
|
||||
return (
|
||||
deployments.loaded() &&
|
||||
deployments
|
||||
.ids()!
|
||||
.filter((id) => deployments.get(id)?.deployment.server_id === p.id)
|
||||
);
|
||||
});
|
||||
const [reloading, setReloading] = createSignal(false);
|
||||
const stats = () => serverStats.get(p.id);
|
||||
const reloadStats = async () => {
|
||||
setReloading(true);
|
||||
await serverStats.load(p.id);
|
||||
setReloading(false);
|
||||
};
|
||||
return (
|
||||
<Show when={server()}>
|
||||
<div class={combineClasses(s.Server, "shadow")}>
|
||||
<button
|
||||
class={combineClasses(s.ServerButton, "shadow")}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<Flex>
|
||||
<Icon type="chevron-down" width="1rem" />
|
||||
<h1 style={{ "font-size": "1.25rem" }}>{server()?.server.name}</h1>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<Show when={width() > 500 && server()?.status === ServerStatus.Ok}>
|
||||
<Show when={stats()} fallback={<Loading type="three-dot" />}>
|
||||
<div>
|
||||
<div style={{ opacity: 0.7 }}>cpu:</div>{" "}
|
||||
{stats()!.cpu_perc.toFixed(1)}%
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ opacity: 0.7 }}>mem:</div>{" "}
|
||||
{(
|
||||
(100 * stats()!.mem_used_gb) /
|
||||
stats()!.mem_total_gb
|
||||
).toFixed(1)}
|
||||
%
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ opacity: 0.7 }}>disk:</div>{" "}
|
||||
{(
|
||||
(100 * stats()!.disk.used_gb) /
|
||||
stats()!.disk.total_gb
|
||||
).toFixed(1)}
|
||||
%
|
||||
</div>
|
||||
<Flex gap=".5rem" alignItems="center">
|
||||
<Show
|
||||
when={!reloading()}
|
||||
fallback={
|
||||
<button class="blue" style={{ height: "fit-content" }}>
|
||||
<Loading type="spinner" scale={0.2} />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
class="blue"
|
||||
style={{ height: "fit-content" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
reloadStats();
|
||||
}}
|
||||
>
|
||||
<Icon type="refresh" width="0.85rem" />
|
||||
</button>
|
||||
</Show>
|
||||
{/* <StatGraphs id={p.id} /> */}
|
||||
</Flex>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={server()?.status !== ServerStatus.Ok}>
|
||||
{/* <StatGraphs id={p.id} /> */}
|
||||
</Show>
|
||||
<div
|
||||
class={
|
||||
server()?.server.enabled
|
||||
? server()?.status === ServerStatus.Ok
|
||||
? "green"
|
||||
: "red"
|
||||
: "blue"
|
||||
}
|
||||
style={{
|
||||
padding: "0.25rem",
|
||||
"border-radius": ".35rem",
|
||||
transition: "background-color 125ms ease-in-out",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/server/${p.id}`);
|
||||
}}
|
||||
>
|
||||
{server()?.status.replaceAll("_", " ").toUpperCase()}
|
||||
</div>
|
||||
</Flex>
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<Grid
|
||||
gap=".5rem"
|
||||
class={combineClasses(s.Deployments, open() ? s.Enter : s.Exit)}
|
||||
>
|
||||
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
|
||||
<Show
|
||||
when={
|
||||
user().admin ||
|
||||
server()?.server.permissions![getId(user())] ===
|
||||
PermissionLevel.Update
|
||||
}
|
||||
>
|
||||
<NewDeployment serverID={p.id} />
|
||||
</Show>
|
||||
</Grid>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Server;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { Update as UpdateType } from "../../../types";
|
||||
import { Operation, Update as UpdateType } from "../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
readableDuration,
|
||||
readableMonitorTimestamp,
|
||||
} from "../../../util/helpers";
|
||||
import { useToggle } from "../../../util/hooks";
|
||||
@@ -26,6 +27,9 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
}
|
||||
};
|
||||
const operation = () => {
|
||||
if (p.update.operation === Operation.BuildBuild) {
|
||||
return "build";
|
||||
}
|
||||
return p.update.operation.replaceAll("_", " ");
|
||||
};
|
||||
const [showLog, toggleShowLog] = useToggle();
|
||||
@@ -56,7 +60,7 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
</Flex>
|
||||
</Grid>
|
||||
<CenterMenu
|
||||
title={operation()}
|
||||
title={`${operation()} | ${name()}`}
|
||||
show={showLog}
|
||||
toggleShow={toggleShowLog}
|
||||
target={<Icon type="console" />}
|
||||
@@ -65,15 +69,29 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
padding="1rem 2rem"
|
||||
content={
|
||||
<Grid class={s.LogContainer} gap="1rem">
|
||||
<Grid gap="0.5rem" class="card lightgrey shadow">
|
||||
<div>
|
||||
started at: {readableMonitorTimestamp(p.update.start_ts)}
|
||||
</div>
|
||||
<Show when={p.update.end_ts}>
|
||||
<div>
|
||||
duration:{" "}
|
||||
{readableDuration(p.update.start_ts, p.update.end_ts!)}
|
||||
</div>
|
||||
</Show>
|
||||
</Grid>
|
||||
<For each={p.update.logs}>
|
||||
{(log, index) => {
|
||||
return (
|
||||
<Grid gap="0.5rem" class="card lightgrey shadow">
|
||||
<Flex alignItems="center">
|
||||
<Flex alignItems="center" class="wrap">
|
||||
<h1>{log.stage}</h1>
|
||||
<div style={{ opacity: 0.7 }}>
|
||||
(stage {index() + 1} of {p.update.logs.length})
|
||||
</div>
|
||||
<div style={{ opacity: 0.7 }}>
|
||||
{readableDuration(log.start_ts, log.end_ts)}
|
||||
</div>
|
||||
</Flex>
|
||||
<div>command</div>
|
||||
<pre class={combineClasses(s.Log)}>{log.command}</pre>
|
||||
|
||||
@@ -1,11 +1,98 @@
|
||||
@use "../../style/colors.scss" as c;
|
||||
|
||||
.Summary {
|
||||
padding: 1rem;
|
||||
.Home {
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.Home {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.SummaryItem {
|
||||
background-color: c.$lightgrey;
|
||||
padding: 1rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.Tabs {
|
||||
// padding: 1rem;
|
||||
// width: 80%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.Container {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.Server {
|
||||
background-color: c.$lightgrey;
|
||||
height: fit-content;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ServerButton {
|
||||
justify-content: space-between;
|
||||
z-index: 5;
|
||||
transition: background-color 500ms ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ServerButton:hover {
|
||||
background-color: rgba(c.$lightblue, 0.5);
|
||||
}
|
||||
|
||||
.Deployments {
|
||||
background-color: c.$lightgrey;
|
||||
transform-origin: top;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.DropdownItem {
|
||||
padding: 0.5rem 1rem;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
transition: background-color 500ms ease;
|
||||
}
|
||||
|
||||
.DropdownItem:hover {
|
||||
background-color: rgba(c.$lightblue, 0.5);
|
||||
}
|
||||
|
||||
.UpdatesContainer {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.Summary {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Updates {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
// @media only screen and (max-width: 1200px) {
|
||||
// .Summary {
|
||||
// width: 80%;
|
||||
// }
|
||||
|
||||
// .Updates {
|
||||
// width: 80%
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media only screen and (max-width: 500px) {
|
||||
// .Summary {
|
||||
// width: 95%;
|
||||
// }
|
||||
|
||||
// .Updates {
|
||||
// width: 95%
|
||||
// }
|
||||
// }
|
||||
@@ -76,6 +76,7 @@ a {
|
||||
background-color: transparent;
|
||||
transition: background-color 500ms ease-in-out;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
||||
@@ -44,6 +44,14 @@ export function readableMonitorTimestamp(rfc3339_ts: string) {
|
||||
} ${pm ? "PM" : "AM"}`;
|
||||
}
|
||||
|
||||
export function readableDuration(start_ts: string, end_ts: string) {
|
||||
const start = new Date(start_ts);
|
||||
const end = new Date(end_ts);
|
||||
const durr = end.getTime() - start.getTime();
|
||||
const seconds = (durr / 1000).toFixed(1);
|
||||
return `${seconds} seconds`
|
||||
}
|
||||
|
||||
export function validatePercentage(perc: string) {
|
||||
// validates that a string represents a percentage
|
||||
const percNum = Number(perc);
|
||||
|
||||
Reference in New Issue
Block a user