Compare commits

...

16 Commits

Author SHA1 Message Date
mbecker20
c6559814b1 frontend for docker build extra args and use buildx 2023-03-31 17:31:39 +00:00
mbecker20
c8c080183f remove publish for cli 2023-03-31 17:04:15 +00:00
mbecker20
597b67f799 0.2.11 support buildx and arbitrary extra args 2023-03-31 17:03:38 +00:00
mbecker20
ec52d5f422 support docker buildx build and passing arbitrary extra args 2023-03-31 16:57:02 +00:00
mbecker20
34806304d6 add center menu title bottom border and adjust copy menu 2023-03-31 05:41:35 +00:00
beckerinj
87953d5495 menu padding 2rem 2023-03-31 01:27:17 -04:00
beckerinj
b6c7c80c95 full width input for copy menu 2023-03-31 01:26:19 -04:00
beckerinj
77e568d5c3 small 2023-03-27 12:41:59 -04:00
mbecker20
699fc51cf7 link to build if click on image deployment header 2023-03-27 15:30:11 +00:00
mbecker20
21029c90b7 info page on stats page 2023-03-27 05:13:12 +00:00
mbecker20
6b0530eb7f brush up server stats page 2023-03-26 23:15:58 +00:00
beckerinj
f7061c7225 toggle to show absolutes for mem and disk stat graphs 2023-03-26 18:47:21 -04:00
mbecker20
750f698369 updates page 2023-03-26 02:20:39 +00:00
mbecker20
ec5ef42298 add max height / scrolling to copy menu target selector 2023-03-24 00:45:47 +00:00
beckerinj
46820b0044 increase the tab title padding 2023-03-23 20:36:31 -04:00
beckerinj
425a6648f7 improve summary styling 2023-03-23 03:13:19 -04:00
43 changed files with 661 additions and 387 deletions

8
.vscode/tasks.json vendored
View File

@@ -100,14 +100,6 @@
"cwd": "${workspaceFolder}/lib/monitor_client"
}
},
{
"type": "cargo",
"command": "publish",
"label": "publish monitor cli",
"options": {
"cwd": "${workspaceFolder}/cli"
}
},
{
"type": "shell",
"command": "docker compose up -d",

34
Cargo.lock generated
View File

@@ -740,7 +740,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "core"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"async_timing_util",
@@ -759,7 +759,7 @@ dependencies = [
"hmac",
"jwt",
"monitor_helpers",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"mungos",
"periphery_client",
"serde",
@@ -993,10 +993,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "db_client"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"mungos",
]
@@ -1857,12 +1857,12 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"async_timing_util",
"clap",
"colored",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"rand",
"run_command",
"serde",
@@ -1874,12 +1874,12 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"envy",
"futures-util",
"monitor_types 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
@@ -1891,11 +1891,11 @@ dependencies = [
[[package]]
name = "monitor_helpers"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"axum",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"rand",
"serde",
"serde_json",
@@ -1904,7 +1904,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"async_timing_util",
@@ -1916,7 +1916,7 @@ dependencies = [
"envy",
"futures",
"monitor_helpers",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"run_command",
"serde",
"serde_derive",
@@ -1930,7 +1930,7 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"bollard",
@@ -1947,9 +1947,9 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bf35db6341431dea9f062f5d676305a213834638410fb9cdc49ca2521635c43"
checksum = "b2b2809cdf9e2c1f1faa0093e6da57e6e4d5833f7dd492df490cc4c66f73a383"
dependencies = [
"anyhow",
"bollard",
@@ -2196,11 +2196,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "periphery_client"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"futures-util",
"monitor_types 0.2.10",
"monitor_types 0.2.11",
"reqwest",
"serde",
"serde_json",

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_cli"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"

View File

@@ -1,6 +1,6 @@
[package]
name = "core"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -12,7 +12,7 @@ use crate::{
state::{State, StateExtension},
};
const NUM_UPDATES_PER_PAGE: usize = 10;
const NUM_UPDATES_PER_PAGE: usize = 20;
pub fn router() -> Router {
Router::new().route(

View File

@@ -11,6 +11,7 @@ const Users = lazy(() => import("./components/users/Users"));
const User = lazy(() => import("./components/users/User"));
const Stats = lazy(() => import("./components/stats/Stats"));
const Account = lazy(() => import("./components/account/Account"));
const Updates = lazy(() => import("./components/Updates"));
const App: Component = () => {
const { user } = useUser();
@@ -19,6 +20,7 @@ const App: Component = () => {
<Topbar />
<Routes>
<Route path="/" component={Home} />
<Route path="/updates" component={Updates} />
<Route path="/build/:id" component={Build} />
<Route path="/deployment/:id" component={Deployment} />
<Route path="/server/:id" component={Server} />

View File

@@ -69,10 +69,10 @@ const CopyMenu: Component<{
targetClass="blue"
content={() => (
<Grid placeItems="center">
<Flex alignItems="center">
<Flex class="full-width" alignItems="center">
<Input
placeholder="copy name"
class="card dark"
class="card dark full-width"
style={{ padding: "0.5rem" }}
value={newName()}
onEdit={setNewName}
@@ -87,6 +87,8 @@ const CopyMenu: Component<{
targetClass="blue"
targetStyle={{ display: "flex", gap: "0.5rem" }}
searchStyle={{ width: "100%" }}
menuClass="scroller"
menuStyle={{ "max-height": "40vh" }}
position="bottom right"
useSearch
/>

View File

@@ -0,0 +1,157 @@
import { A } from "@solidjs/router";
import {
Component,
createEffect,
createMemo,
createSignal,
For,
Show,
} from "solid-js";
import { OPERATIONS } from "..";
import { useAppDimensions } from "../state/DimensionProvider";
import { useAppState } from "../state/StateProvider";
import { Operation, Update as UpdateType, UpdateStatus } from "../types";
import { readableMonitorTimestamp, readableVersion } from "../util/helpers";
import Icon from "./shared/Icon";
import Input from "./shared/Input";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import Loading from "./shared/loading/Loading";
import Selector from "./shared/menu/Selector";
import UpdateMenu from "./update/UpdateMenu";
const Updates: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const { updates, usernames, name_from_update_target } = useAppState();
const [operation, setOperation] = createSignal<Operation>();
createEffect(() => {
if (operation()) {
updates.load([operation()!]);
} else {
updates.load();
}
});
const [search, setSearch] = createSignal("");
const filtered_updates = createMemo(() => {
return updates.collection()?.filter((u) => {
const name = name_from_update_target(u.target);
if (name.includes(search())) return true;
const username = usernames.get(u.operator);
if (username?.includes(search())) return true;
});
});
return (
<Grid class="full-width card shadow">
<Flex alignItems="center" justifyContent="space-between">
<h1>updates</h1>
<Flex alignItems="center">
<Input class="lightgrey" placeholder="search" onEdit={setSearch} />
<Selector
label={isMobile() ? undefined : "operation: "}
selected={operation() ? operation()! : "all"}
items={["all", ...OPERATIONS]}
onSelect={(o) =>
o === "all"
? setOperation(undefined)
: setOperation(o.replaceAll(" ", "_") as Operation)
}
targetClass="blue"
position="bottom right"
searchStyle={{ width: "15rem" }}
menuClass="scroller"
menuStyle={{ "max-height": "50vh" }}
useSearch
/>
</Flex>
</Flex>
<Show
when={updates.loaded()}
fallback={
<Flex justifyContent="center">
<Loading type="three-dot" />
</Flex>
}
>
<For each={filtered_updates()}>
{(update) => <Update update={update} />}
</For>
<Show when={!updates.noMore()}>
<button
class="grey full-width"
onClick={() =>
operation()
? updates.loadMore([operation()!])
: updates.loadMore()
}
>
load more
</button>
</Show>
</Show>
</Grid>
);
};
export default Updates;
const Update: Component<{ update: UpdateType }> = (p) => {
const { isMobile } = useAppDimensions();
const { usernames, name_from_update_target } = useAppState();
const name = () => name_from_update_target(p.update.target);
const operation = () => {
if (p.update.operation === Operation.BuildBuild) {
return `build ${readableVersion(p.update.version!)}`;
}
return `${p.update.operation.replaceAll("_", " ")}${
p.update.version ? " " + readableVersion(p.update.version) : ""
}`;
};
const link_to = () => {
return p.update.target.type === "System"
? "/"
: `/${p.update.target.type.toLowerCase()}/${p.update.target.id}`;
};
return (
<Flex
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<Flex
alignItems="center"
justifyContent="space-between"
style={{ width: isMobile() ? "100%" : undefined }}
>
<A style={{ padding: 0 }} href={link_to()}>
<h2 class="text-hover">{name()}</h2>
</A>
<div
style={{
color: !p.update.success ? "rgb(182, 47, 52)" : "inherit",
}}
>
{operation()}
</div>
<Show when={p.update.status === UpdateStatus.InProgress}>
<div style={{ opacity: 0.7 }}>(in progress)</div>
</Show>
</Flex>
<Flex
alignItems="center"
justifyContent="space-between"
style={{ width: isMobile() ? "100%" : undefined }}
>
<Flex gap="0.5rem">
<Icon type="user" />
<div>{usernames.get(p.update.operator)}</div>
</Flex>
<Flex alignItems="center">
<div style={{ "place-self": "center end" }}>
{readableMonitorTimestamp(p.update.start_ts)}
</div>
<UpdateMenu update={p.update} />
</Flex>
</Flex>
</Flex>
);
};

View File

@@ -11,6 +11,8 @@ import BuildArgs from "./BuildArgs";
import Version from "./Version";
import Repo from "./Repo";
import WebhookUrl from "./WebhookUrl";
import ExtraArgs from "./ExtraArgs";
import UseBuildx from "./UseBuildx";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
@@ -23,6 +25,8 @@ const BuildConfig: Component<{}> = (p) => {
<Docker />
<CliBuild />
<BuildArgs />
<ExtraArgs />
<UseBuildx />
<Show when={userCanUpdate()}>
<WebhookUrl />
</Show>

View File

@@ -0,0 +1,59 @@
import { Component, For, Show } from "solid-js";
import Icon from "../../../shared/Icon";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
const ExtraArgs: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const onAdd = () => {
setBuild("docker_build_args", "extra_args", (extra_args: any) => [
...extra_args,
"",
]);
};
const onRemove = (index: number) => {
setBuild("docker_build_args", "extra_args", (extra_args) =>
extra_args!.filter((_, i) => i !== index)
);
};
return (
<Grid class="config-item shadow">
<Flex justifyContent="space-between" alignItems="center">
<h1>extra args</h1>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={[...build.docker_build_args!.extra_args!.keys()]}>
{(_, index) => (
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<Input
placeholder="--extra-arg=value"
value={build.docker_build_args!.extra_args![index()]}
style={{ width: "80%" }}
onEdit={(value) =>
setBuild("docker_build_args", "extra_args", index(), value)
}
disabled={!userCanUpdate()}
/>
<Show when={userCanUpdate()}>
<button class="red" onClick={() => onRemove(index())}>
<Icon type="minus" />
</button>
</Show>
</Flex>
)}
</For>
</Grid>
);
};
export default ExtraArgs;

View File

@@ -0,0 +1,30 @@
import { Component, Show } from "solid-js";
import Flex from "../../../shared/layout/Flex";
import { useConfig } from "../Provider";
const UseBuildx: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const use_buildx = () => build.docker_build_args?.use_buildx || false;
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>use buildx</h1>
<Show
when={userCanUpdate()}
fallback={<div>{use_buildx() ? "enabled" : "disabled"}</div>}
>
<button
class={use_buildx() ? "green" : "red"}
onClick={() => setBuild("docker_build_args", "use_buildx", (c) => !c)}
>
{use_buildx() ? "enabled" : "disabled"}
</button>
</Show>
</Flex>
);
};
export default UseBuildx;

View File

@@ -119,7 +119,18 @@ const Header: Component<{}> = (p) => {
/>
</Show>
</Show>
<div style={{ opacity: 0.7 }}>{image()}</div>
<Show
when={deployment().deployment.build_id}
fallback={<div style={{ opacity: 0.7 }}>{image()}</div>}
>
<A
href={`/build/${deployment().deployment.build_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{image()}
</A>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<Flex alignItems="center">

View File

@@ -14,26 +14,29 @@ const Summary: Component<{}> = (p) => {
const serverCount = useServerCount();
return (
<Grid
class="card shadow"
class="full-size"
gridTemplateColumns={isMobile() ? "1fr" : "1fr 1fr"}
style={{
width: "100%",
height: "100%",
"box-sizing": "border-box",
}}
placeItems="center"
gap="0"
>
<div
style={{ width: `${PIE_CHART_SIZE}px`, height: `${PIE_CHART_SIZE}px` }}
>
<PieChart title="deployments" sections={deployentCount()} />
</div>
<div
style={{ width: `${PIE_CHART_SIZE}px`, height: `${PIE_CHART_SIZE}px` }}
>
<PieChart title="servers" sections={serverCount()} />
</div>
<Grid class="card shadow full-size" placeItems="center">
<div
style={{
width: `${PIE_CHART_SIZE}px`,
height: `${PIE_CHART_SIZE}px`,
}}
>
<PieChart title="deployments" sections={deployentCount()} />
</div>
</Grid>
<Grid class="card shadow full-size" placeItems="center">
<div
style={{
width: `${PIE_CHART_SIZE}px`,
height: `${PIE_CHART_SIZE}px`,
}}
>
<PieChart title="servers" sections={serverCount()} />
</div>
</Grid>
</Grid>
);
};

View File

@@ -1,166 +0,0 @@
import { Component, createMemo, For, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { DockerContainerState, ServerStatus } from "../../types";
import Grid from "../shared/layout/Grid";
import Flex from "../shared/layout/Flex";
const Summary: Component<{}> = (p) => {
return (
<Grid class="card shadow" gridTemplateRows="auto 1fr 1fr 1fr">
<h1>summary</h1>
<DeploymentsSummary />
<ServersSummary />
<BuildsSummary />
</Grid>
);
};
export default Summary;
const SummaryItem: Component<{
title: string;
metrics: Array<{ title: string; class: string; count?: number }>;
}> = (p) => {
return (
<Flex
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<h2>{p.title}</h2>
<Flex class="wrap">
<For each={p.metrics}>
{(metric) => (
<Show when={metric?.count && metric.count > 0}>
<Flex gap="0.4rem" alignItems="center">
<div>{metric.title}</div>
<h2 class={metric.class}>{metric.count}</h2>
</Flex>
</Show>
)}
</For>
</Flex>
</Flex>
);
};
const BuildsSummary = () => {
const { builds } = useAppState();
return (
<SummaryItem
title="builds"
metrics={[
{ title: "total", class: "text-green", count: builds.ids()?.length },
]}
/>
);
};
const DeploymentsSummary = () => {
const deployentCount = useDeploymentCount();
return (
<SummaryItem
title="deployments"
metrics={[
{
title: "total",
class: "text-green",
count: deployentCount().total,
},
{
title: "running",
class: "text-green",
count: deployentCount().running,
},
{
title: "stopped",
class: "text-red",
count: deployentCount().stopped,
},
{
title: "not deployed",
class: "text-blue",
count: deployentCount().notDeployed,
},
{
title: "unknown",
class: "text-blue",
count: deployentCount().unknown,
},
]}
/>
);
};
const ServersSummary = () => {
const serverCount = useServerCount();
return (
<SummaryItem
title="servers"
metrics={[
{ title: "total", class: "text-green", count: serverCount().total },
{ title: "healthy", class: "text-green", count: serverCount().healthy },
{
title: "unhealthy",
class: "text-red",
count: serverCount().unhealthy,
},
{
title: "disabled",
class: "text-blue",
count: serverCount().disabled,
},
]}
/>
);
};
function useDeploymentCount() {
const { deployments } = useAppState();
const count = createMemo(() => {
const ids = deployments.ids();
if (!ids)
return { total: 0, running: 0, stopped: 0, notDeployed: 0, unknown: 0 };
let running = 0;
let stopped = 0;
let notDeployed = 0;
let unknown = 0;
for (const id of ids) {
const state = deployments.get(id)!.state;
if (state === DockerContainerState.NotDeployed) {
notDeployed++;
} else if (state === DockerContainerState.Running) {
running++;
} else if (state === DockerContainerState.Exited) {
stopped++;
} else if (state === DockerContainerState.Unknown) {
unknown++;
}
}
return { total: ids.length, running, stopped, notDeployed, unknown };
});
return count;
}
function useServerCount() {
const { servers } = useAppState();
const count = createMemo(() => {
const ids = servers.ids();
if (!ids) return { total: 0, healthy: 0, unhealthy: 0, disabled: 0 };
let healthy = 0;
let unhealthy = 0;
let disabled = 0;
for (const id of ids) {
const server = servers.get(id)!;
if (server.status === ServerStatus.Disabled) {
disabled++;
} else if (server.status === ServerStatus.Ok) {
healthy++;
} else if (server.status === ServerStatus.NotOk) {
unhealthy++;
}
}
return { total: ids.length, healthy, unhealthy, disabled };
});
return count;
}

View File

@@ -14,18 +14,9 @@ import UpdateMenu from "../../update/UpdateMenu";
import s from "./update.module.scss";
const Update: Component<{ update: UpdateType }> = (p) => {
const { deployments, servers, builds, usernames } = useAppState();
const name = () => {
if (p.update.target.type === "Deployment" && deployments.loaded()) {
return deployments.get(p.update.target.id!)?.deployment.name || "deleted";
} else if (p.update.target.type === "Server" && servers.loaded()) {
return servers.get(p.update.target.id)?.server.name || "deleted";
} else if (p.update.target.type === "Build" && builds.loaded()) {
return builds.get(p.update.target.id)?.name || "deleted";
} else {
return "monitor";
}
};
const { usernames, name_from_update_target } =
useAppState();
const name = () => name_from_update_target(p.update.target);
const operation = () => {
if (p.update.operation === Operation.BuildBuild) {
return `build ${readableVersion(p.update.version!)}`;

View File

@@ -1,4 +1,6 @@
import { A } from "@solidjs/router";
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { OPERATIONS } from "../../..";
import { useAppState } from "../../../state/StateProvider";
import { Operation } from "../../../types";
import Flex from "../../shared/layout/Flex";
@@ -7,10 +9,6 @@ import Loading from "../../shared/loading/Loading";
import Selector from "../../shared/menu/Selector";
import Update from "./Update";
const OPERATIONS = Object.values(Operation)
.filter((e) => e !== "none" && !e.includes("user"))
.map((e) => e.replaceAll("_", " "));
const Updates: Component<{}> = () => {
const { updates } = useAppState();
const [operation, setOperation] = createSignal<Operation>();
@@ -24,8 +22,11 @@ const Updates: Component<{}> = () => {
return (
<Grid class="card shadow" style={{ "flex-grow": 1 }}>
<Flex alignItems="center" justifyContent="space-between">
<h1>updates</h1>
<A href="/updates" style={{ padding: 0 }}>
<h1>updates</h1>
</A>
<Selector
label="operation: "
selected={operation() ? operation()! : "all"}
items={["all", ...OPERATIONS]}
onSelect={(o) =>
@@ -50,7 +51,7 @@ const Updates: Component<{}> = () => {
}
>
<Grid class="updates-container-small scroller">
<For each={updates.collection()!}>
<For each={updates.collection()}>
{(update) => <Update update={update} />}
</For>
<Show when={!updates.noMore()}>

View File

@@ -7,7 +7,6 @@ import { readableStorageAmount } from "../../../util/helpers";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Loading from "../../shared/loading/Loading";
import HoverMenu from "../../shared/menu/HoverMenu";
const Info: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();

View File

@@ -76,7 +76,7 @@ const Child: Component<{
>
<Grid
class={combineClasses(s.Menu, "shadow")}
style={{ padding: (p.padding as any) || "1rem", ...p.style }}
style={{ padding: (p.padding as any) || "2rem", ...p.style }}
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>

View File

@@ -37,10 +37,10 @@ const Selector: Component<{
}> = (p) => {
const [show, toggle] = useToggle();
const [search, setSearch] = createSignal("");
let ref: HTMLInputElement | undefined;
let search_ref: HTMLInputElement | undefined;
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected);
createEffect(() => {
if (show()) setTimeout(() => ref?.focus(), 200);
if (show()) setTimeout(() => search_ref?.focus(), 200);
});
return (
<Show
@@ -70,7 +70,7 @@ const Selector: Component<{
<>
<Show when={p.useSearch}>
<Input
ref={ref}
ref={search_ref}
placeholder="search"
value={search()}
onEdit={setSearch}

View File

@@ -23,6 +23,7 @@
width: fit-content;
/* border: solid 1px rgba(2, 107, 121, 0.25); */
background-color: c.$grey;
border: solid c.$darkgrey 2px;
z-index: 21;
border-radius: 0.25rem;
box-sizing: border-box;
@@ -142,6 +143,11 @@ $anim-time: 350ms;
background-color: rgba(0, 0, 0, 0.4);
}
.CenterMenuHeader {
border-bottom: solid rgba(c.$lightgrey, 0.9) 2px;
padding-bottom: 1rem;
}
.SelectorItem:hover {
background-color: c.$lightgrey;
}

View File

@@ -9,7 +9,7 @@
.TabTitle {
display: grid;
place-items: center;
padding: 0.25rem 0.5rem;
padding: 0.75rem;
border-radius: 0rem;
cursor: pointer;
font-size: 1rem;

View File

@@ -1,10 +1,19 @@
import { Accessor, Component, For, ParentComponent, Show } from "solid-js";
import {
Accessor,
Component,
For,
JSXElement,
ParentComponent,
Show,
} from "solid-js";
import { COLORS } from "../../style/colors";
import { SystemStats, SystemStatsRecord } from "../../types";
import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../util/helpers";
import { useLocalStorage, useLocalStorageToggle } from "../../util/hooks";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";
import s from "./stats.module.scss";
@@ -15,13 +24,18 @@ const SMALL_CHART_HEIGHT = "150px";
const SingleStatChart: Component<{
line?: LightweightValue[];
header: string;
headerRight?: JSXElement;
label: string;
color: string;
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
return (
<StatChartContainer header={p.header} small={p.small}>
<StatChartContainer
header={p.header}
headerRight={p.headerRight}
small={p.small}
>
<Show when={p.line}>
<LightweightChart
class={s.LightweightChart}
@@ -44,23 +58,25 @@ const SingleStatChart: Component<{
const StatChartContainer: ParentComponent<{
header: string;
headerRight?: JSXElement;
small?: boolean;
}> = (p) => {
return (
<Grid
gap="0.5rem"
class="card shadow"
class="card shadow full-width"
style={{
height: "fit-content",
width: "100%",
"box-sizing": "border-box",
"padding-top": "0.5rem",
"padding-bottom": "0.2rem",
}}
>
<Show when={!p.small} fallback={<div>{p.header}</div>}>
<h2>{p.header}</h2>
</Show>
<Flex justifyContent="space-between">
<Show when={!p.small} fallback={<div>{p.header}</div>}>
<h2>{p.header}</h2>
</Show>
{p.headerRight}
</Flex>
{p.children}
</Grid>
);
@@ -152,20 +168,42 @@ export const MemChart: Component<{
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
const [absolute, toggleAbsolute] = useLocalStorageToggle("stats-mem-mode-v2");
const symbol = () => (absolute() ? "GiB" : "%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.mem_used_gb) / s.mem_total_gb,
};
});
if (absolute()) {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: s.mem_used_gb,
};
});
} else {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.mem_used_gb) / s.mem_total_gb,
};
});
}
};
return (
<SingleStatChart
header="memory"
label="mem %"
headerRight={
<button
class="green"
style={{ padding: "0.2rem" }}
onClick={toggleAbsolute}
>
{symbol()}
</button>
}
label={`mem ${symbol()}`}
color={COLORS.green}
line={line()}
small={p.small}
@@ -179,20 +217,43 @@ export const DiskChart: Component<{
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
const [absolute, toggleAbsolute] =
useLocalStorageToggle("stats-disk-mode-v2");
const symbol = () => (absolute() ? "GiB" : "%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.disk.used_gb) / s.disk.total_gb,
};
});
if (absolute()) {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: s.disk.used_gb,
};
});
} else {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.disk.used_gb) / s.disk.total_gb,
};
});
}
};
return (
<SingleStatChart
header="disk"
label="disk %"
headerRight={
<button
class="orange"
style={{ padding: "0.2rem" }}
onClick={toggleAbsolute}
>
{symbol()}
</button>
}
label={`disk ${symbol()}`}
color={COLORS.orange}
line={line()}
small={p.small}

View File

@@ -29,20 +29,23 @@ const HistoricalStats: Component<{
const params = useParams();
const { timelength, page } = useStatsState();
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
createEffect(() => {
client
const [loading, setLoading] = createSignal(false);
createEffect(async () => {
setLoading(true);
const stats = await client
.get_server_stats_history(params.id, {
interval: timelength(),
page: page(),
limit: 500,
networks: true,
components: true,
})
.then(setStats);
});
setStats(stats);
setLoading(false);
});
return (
<Grid class={s.Content} placeItems="start center">
<Show when={stats()} fallback={<Loading type="three-dot" />}>
<Show when={stats() && !loading()} fallback={<Loading type="three-dot" />}>
<SimpleTabs
localStorageKey="historical-stats-view-v3"
defaultSelected="basic"

View File

@@ -1,12 +1,19 @@
import { useParams } from "@solidjs/router";
import { ParentComponent, createContext, useContext, createSignal, createResource } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { SystemInformation, Timelength } from "../../types";
import { useLocalStorage } from "../../util/hooks";
export enum StatsView {
Current = "current",
Historical = "historical",
Info = "info"
}
const value = () => {
const params = useParams();
const [view, setView] = useLocalStorage("current", "stats-view-v1");
const [view, setView] = useLocalStorage(StatsView.Current, "stats-view-v2");
const [timelength, setTimelength] = useLocalStorage(
Timelength.OneMinute,
"stats-timelength-v3"
@@ -16,12 +23,7 @@ const value = () => {
`${params.id}-stats-poll-v3`
);
const [page, setPage] = createSignal(0);
// const [wsOpen, setWsOpen] = createSignal(false);
const [sysInfo] = createResource<SystemInformation>(() =>
client.get_server_system_info(params.id)
);
return {
sysInfo,
view,
setView,
timelength,

View File

@@ -1,20 +1,16 @@
import { A, useParams } from "@solidjs/router";
import {
Component,
Match,
Show,
Switch,
} from "solid-js";
import { MAX_PAGE_WIDTH } from "../..";
import { Component, createResource, For, Match, Show, Switch } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { ServerStatus, Timelength } from "../../types";
import { readableStorageAmount } from "../../util/helpers";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Selector from "../shared/menu/Selector";
import CurrentStats from "./CurrentStats";
import HistoricalStats from "./HistoricalStats";
import { StatsProvider, useStatsState } from "./Provider";
import { StatsProvider, useStatsState, StatsView } from "./Provider";
const TIMELENGTHS = [
Timelength.FifteenSeconds,
@@ -38,115 +34,182 @@ const Stats = () => {
const StatsComp: Component<{}> = () => {
const { view } = useStatsState();
return (
<Grid
style={{
width: "100%",
"box-sizing": "border-box",
}}
>
<Flex justifyContent="space-between" style={{ width: "100%" }}>
<Header />
<SysInfo />
</Flex>
<Show when={view() === "historical"}>
<Grid class="full-width">
<Header />
<Show when={view() === StatsView.Historical}>
<Flex alignItems="center" style={{ "place-self": "center" }}>
<PageManager />
</Flex>
</Show>
<Switch>
<Match when={view() === "current"}>
<Match when={view() === StatsView.Current}>
<CurrentStats />
</Match>
<Match when={view() === "historical"}>
<Match when={view() === StatsView.Historical}>
<HistoricalStats />
</Match>
<Match when={view() === StatsView.Info}>
<SysInfo />
</Match>
</Switch>
</Grid>
);
};
export const Header: Component<{}> = (p) => {
const { servers } = useAppState();
const { servers, serverInfo } = useAppState();
const params = useParams();
const server = () => servers.get(params.id);
const { view, setView, timelength, setTimelength, setPage, pollRate, setPollRate } = useStatsState();
const {
view,
setView,
timelength,
setTimelength,
setPage,
pollRate,
setPollRate,
} = useStatsState();
const sysInfo = () => serverInfo.get(params.id);
return (
<Flex alignItems="center" style={{ height: "fit-content" }}>
<h1>{server()?.server.name}</h1>
<A
href={`/server/${params.id}`}
class={
server()?.server.enabled
? server()?.status === ServerStatus.Ok
? "green"
: "red"
: "blue"
}
style={{
"border-radius": ".35rem",
transition: "background-color 125ms ease-in-out",
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{server()?.status.replaceAll("_", " ").toUpperCase()}
</A>
<Grid gap="0" gridTemplateColumns="repeat(2, 1fr)">
<button
class={view() === "current" ? "selected" : "grey"}
style={{ width: "100%" }}
onClick={() => setView("current")}
>
current
</button>
<button
class={view() === "historical" ? "selected" : "grey"}
style={{ width: "100%" }}
onClick={() => setView("historical")}
>
historical
</button>
</Grid>
<Show when={view() === "historical"}>
<Selector
targetClass="grey"
selected={timelength()}
items={TIMELENGTHS}
onSelect={(selected) => {
setPage(0);
setTimelength(selected as Timelength);
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" style={{ height: "fit-content" }}>
<h1>{server()?.server.name}</h1>
<A
href={`/server/${params.id}`}
class={
server()?.server.enabled
? server()?.status === ServerStatus.Ok
? "green"
: "red"
: "blue"
}
style={{
"border-radius": ".35rem",
transition: "background-color 125ms ease-in-out",
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{server()?.status.replaceAll("_", " ").toUpperCase()}
</A>
<Selector
targetClass="blue"
selected={view()}
items={Object.values(StatsView)}
onSelect={(v) => setView(v as StatsView)}
position="bottom right"
/>
</Show>
<Show when={view() === "current"}>
<Flex gap="0.5rem" alignItems="center">
<div>poll:</div>
<Show when={view() === "historical"}>
<Selector
targetClass="grey"
selected={timelength()}
items={TIMELENGTHS}
itemMap={(t) => t.replaceAll("-", " ")}
itemClass="full-width"
onSelect={(selected) => {
setPage(0);
setTimelength(selected as Timelength);
}}
position="bottom right"
/>
</Show>
<Show when={view() === "current"}>
<Selector
targetClass="grey"
label="poll: "
selected={pollRate()}
items={[Timelength.OneSecond, Timelength.FiveSeconds]}
onSelect={(selected) => {
setPollRate(selected as Timelength);
}}
position="bottom right"
/>
</Flex>
</Show>
</Show>
</Flex>
<Flex>
<div>{sysInfo()?.cpu_brand}</div>
<div>
{sysInfo()?.core_count} core
{sysInfo()?.core_count && sysInfo()?.core_count! > 1 ? "s" : ""}
</div>
</Flex>
</Flex>
);
};
const SysInfo = () => {
const { sysInfo } = useStatsState();
const { serverInfo } = useAppState();
const params = useParams();
const sysInfo = () => serverInfo.get(params.id);
const [stats] = createResource(() =>
client.get_server_stats(params.id, { disks: true })
);
const os_cards = () => {
return [
{
label: "os",
info: sysInfo()?.os,
},
{
label: "kernel",
info: sysInfo()?.kernel,
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
const cpu_cards = () => {
return [
{
label: "cpu",
info: sysInfo()?.cpu_brand,
},
{
label: "core count",
info: `${sysInfo()?.core_count} cores`,
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
const stats_cards = () => {
return [
{
label: "mem",
info:
stats()?.mem_total_gb &&
readableStorageAmount(stats()?.mem_total_gb!),
},
{
label: "disk",
info:
stats()?.disk.total_gb &&
readableStorageAmount(stats()?.disk.total_gb!),
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
return (
<Flex
alignItems="center"
style={{ "place-self": "center end", width: "fit-content" }}
>
<div>{sysInfo()?.os}</div>
{/* <div>{sysInfo()?.kernel}</div> */}
<div>{sysInfo()?.cpu_brand}</div>
<div>{sysInfo()?.core_count} cores</div>
<Grid class="full-width" placeItems="center">
<Show when={sysInfo()?.host_name}>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<InfoCard info={{ label: "hostname", info: sysInfo()?.host_name! }} />
</Grid>
</Show>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={os_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={cpu_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={stats_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
</Grid>
);
};
const InfoCard: Component<{ info: { label: string; info: string } }> = (p) => {
return (
<Flex class="full-width" justifyContent="space-between">
<h2>{p.info.label}</h2>
<div>{p.info.info}</div>
</Flex>
);
};

View File

@@ -58,7 +58,8 @@ export const Search: Component<{}> = (p) => {
>
<Input
ref={inputRef}
class={s.SearchInput}
class="lightgrey"
style={{ width: "30rem" }}
placeholder="search"
value={search.value()}
onEdit={input.onEdit}

View File

@@ -11,6 +11,7 @@ import { UserProvider } from "./state/UserProvider";
import { Client } from "./util/client";
import { Router } from "@solidjs/router";
import { AppStateProvider } from "./state/StateProvider";
import { Operation } from "./types";
export const TOPBAR_HEIGHT = 50;
export const MAX_PAGE_WIDTH = 1200;
@@ -29,6 +30,10 @@ const token =
export const client = new Client(MONITOR_BASE_URL, token);
export const OPERATIONS = Object.values(Operation)
.filter((e) => e !== "none" && !e.includes("user"))
.map((e) => e.replaceAll("_", " "));
export const { Notifications, pushNotification } = makeNotifications();
client.initialize().then(() => {

View File

@@ -17,7 +17,7 @@ import {
} from "./hooks";
import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { AwsBuilderConfig, PermissionLevel } from "../types";
import { AwsBuilderConfig, PermissionLevel, UpdateTarget } from "../types";
import { client } from "..";
export type State = {
@@ -42,6 +42,7 @@ export type State = {
aws_builder_config: Resource<AwsBuilderConfig>;
docker_organizations: Resource<string[]>;
github_webhook_base_url: Resource<string>;
name_from_update_target: (target: UpdateTarget) => string;
};
const context = createContext<
@@ -148,6 +149,17 @@ export const AppStateProvider: ParentComponent = (p) => {
aws_builder_config,
docker_organizations,
github_webhook_base_url,
name_from_update_target: (target) => {
if (target.type === "Deployment" && deployments) {
return deployments.get(target.id!)?.deployment.name || "deleted";
} else if (target.type === "Server" && servers) {
return servers.get(target.id)?.server.name || "deleted";
} else if (target.type === "Build" && builds) {
return builds.get(target.id)?.name || "deleted";
} else {
return "admin";
}
}
};
// createEffect(() => {

View File

@@ -288,7 +288,7 @@ export function useUpdates(target?: UpdateTarget, show_builds?: boolean) {
operations
);
updates.addManyToEnd(newUpdates);
if (newUpdates.length !== 10) {
if (newUpdates.length !== 20) {
setNoMore(true);
}
}

View File

@@ -318,6 +318,22 @@ svg {
opacity: 0.7;
}
.full-size {
width: 100%;
height: 100%;
box-sizing: border-box;
}
.full-width {
width: 100%;
box-sizing: border-box;
}
.full-height {
height: 100%;
box-sizing: border-box;
}
// .hoverable {
// transition: all 250ms ease-in-out;
// }

View File

@@ -56,6 +56,8 @@ export interface DockerBuildArgs {
build_path: string;
dockerfile_path?: string;
build_args?: EnvironmentVar[];
extra_args?: string[];
use_buildx?: boolean;
}
export interface BuildVersionsReponse {

View File

@@ -1,8 +1,14 @@
import {
Build,
Deployment,
DeploymentWithContainerState,
DockerContainerState,
EnvironmentVar,
Server,
ServerStatus,
ServerWithStatus,
Timelength,
UpdateTarget,
User,
Version,
} from "../types";
@@ -238,10 +244,10 @@ export function readableVersion(version: Version) {
export function readableUserType(user: User) {
if (user.github_id) {
return "github"
return "github";
} else if (user.google_id) {
return "google"
return "google";
} else {
return "local"
return "local";
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "db_client"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_helpers"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "helpers used as dependency for mogh tech monitor"

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_client"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "a client to interact with the monitor system"
@@ -9,8 +9,7 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monitor_types = "0.2.10"
# monitor_types = { path = "../types" }
monitor_types = "0.2.11"
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = { version = "1.25", features = ["full"] }

View File

@@ -1,6 +1,6 @@
[package]
name = "periphery_client"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_types"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "types for the mogh tech monitor"

View File

@@ -155,6 +155,12 @@ pub struct DockerBuildArgs {
#[serde(default)]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
#[serde(default)]
#[builder(default)]
pub use_buildx: bool,
}
#[typeshare]

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_periphery"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "monitor periphery binary"

View File

@@ -6,7 +6,7 @@ use types::{Build, DockerBuildArgs, EnvironmentVar, Log, Version};
use crate::helpers::run_monitor_command;
use super::docker_login;
use super::{docker_login, parse_extra_args};
pub async fn prune_images() -> Log {
let command = format!("docker image prune -a -f");
@@ -32,6 +32,8 @@ pub async fn build(
build_path,
dockerfile_path,
build_args,
extra_args,
use_buildx,
} = docker_build_args
.as_ref()
.ok_or(anyhow!("build missing docker build args"))?;
@@ -46,6 +48,12 @@ pub async fn build(
None => "Dockerfile".to_owned(),
};
let build_args = parse_build_args(build_args);
let extra_args = parse_extra_args(extra_args);
let buildx = if *use_buildx {
" buildx"
} else {
""
};
let image_name = get_image_name(&name, docker_account, docker_organization);
let image_tags = image_tags(&image_name, &version);
let docker_push = if using_account {
@@ -54,7 +62,7 @@ pub async fn build(
String::new()
};
let command = format!(
"cd {} && docker build {build_args}{image_tags} -f {dockerfile_path} .{docker_push}",
"cd {} && docker{buildx} build {build_args}{extra_args}{image_tags} -f {dockerfile_path} .{docker_push}",
build_dir.display()
);
if *skip_secret_interp {

View File

@@ -7,7 +7,7 @@ use types::{
Conversion, Deployment, DockerContainerStats, DockerRunArgs, EnvironmentVar, Log, RestartMode,
};
use crate::helpers::run_monitor_command;
use crate::helpers::{run_monitor_command, docker::parse_extra_args};
use super::docker_login;
@@ -197,12 +197,3 @@ fn parse_post_image(post_image: &Option<String>) -> String {
String::new()
}
}
fn parse_extra_args(extra_args: &Vec<String>) -> String {
let args = extra_args.join(" ");
if args.len() > 0 {
format!(" {args}")
} else {
args
}
}

View File

@@ -38,3 +38,12 @@ pub async fn docker_login(
Ok(false)
}
}
fn parse_extra_args(extra_args: &Vec<String>) -> String {
let args = extra_args.join(" ");
if args.len() > 0 {
format!(" {args}")
} else {
args
}
}

View File

@@ -108,8 +108,7 @@ pub async fn test_build(monitor: &MonitorClient) -> anyhow::Result<Update> {
});
build.docker_build_args = Some(DockerBuildArgs {
build_path: "periphery".to_string(),
dockerfile_path: None,
build_args: Vec::new(),
..Default::default()
});
let build = monitor.update_build(build).await?;
println!("updated build.");