forked from github-starred/komodo
implement home, servers, implement container log passthrough
This commit is contained in:
@@ -185,7 +185,7 @@ impl State {
|
||||
user: &RequestUser,
|
||||
query: impl Into<Option<Document>>,
|
||||
) -> anyhow::Result<Vec<Build>> {
|
||||
let mut builds: Vec<Build> = self
|
||||
let builds: Vec<Build> = self
|
||||
.db
|
||||
.builds
|
||||
.get_some(query, None)
|
||||
@@ -201,7 +201,6 @@ impl State {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
builds.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
Ok(builds)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use helpers::handle_anyhow_error;
|
||||
use mungos::{Deserialize, Document, Serialize};
|
||||
use types::{
|
||||
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
|
||||
DockerContainerState, PermissionLevel, Server,
|
||||
DockerContainerState, Log, PermissionLevel, Server,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -40,6 +40,12 @@ pub struct CopyDeploymentBody {
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetContainerLogQuery {
|
||||
tail: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
@@ -239,6 +245,21 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/log",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>,
|
||||
Query(query): Query<GetContainerLogQuery>| async move {
|
||||
let log = state
|
||||
.get_deployment_container_log(&deployment_id.id, &user, query.tail)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(log))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -301,7 +322,7 @@ impl State {
|
||||
.into_iter()
|
||||
.map(|(container, server_id)| (server_id, container.ok()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut res = deployments
|
||||
let deployments_with_containers = deployments
|
||||
.into_iter()
|
||||
.map(|deployment| {
|
||||
let (state, container) = match containers.get(&deployment.server_id).unwrap() {
|
||||
@@ -324,13 +345,7 @@ impl State {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<DeploymentWithContainerState>>();
|
||||
res.sort_by(|a, b| {
|
||||
a.deployment
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.deployment.name.to_lowercase())
|
||||
});
|
||||
Ok(res)
|
||||
Ok(deployments_with_containers)
|
||||
}
|
||||
|
||||
async fn get_deployment_action_states(
|
||||
@@ -338,7 +353,7 @@ impl State {
|
||||
id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<DeploymentActionState> {
|
||||
self.get_server_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
self.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let action_state = self
|
||||
.deployment_action_states
|
||||
@@ -349,4 +364,21 @@ impl State {
|
||||
.clone();
|
||||
Ok(action_state)
|
||||
}
|
||||
|
||||
async fn get_deployment_container_log(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
tail: Option<u64>,
|
||||
) -> anyhow::Result<Log> {
|
||||
let deployment = self
|
||||
.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let server = self.db.get_server(&deployment.server_id).await?;
|
||||
let log = self
|
||||
.periphery
|
||||
.container_log(&server, &deployment.name, tail)
|
||||
.await?;
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,14 +330,7 @@ impl State {
|
||||
|
||||
ServerWithStatus { server, status }
|
||||
});
|
||||
let mut servers: Vec<ServerWithStatus> = join_all(futures).await;
|
||||
servers.sort_by(|a, b| {
|
||||
a.server
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.server.name.to_lowercase())
|
||||
});
|
||||
Ok(servers)
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
|
||||
async fn get_server_stats(
|
||||
|
||||
@@ -1,69 +1,30 @@
|
||||
import { Component, createMemo, createSignal, For, Match, Switch } 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 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 Servers from "./Tree/Servers";
|
||||
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 (
|
||||
return (
|
||||
<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 />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Servers />
|
||||
<Grid>
|
||||
<Summary />
|
||||
<Updates />
|
||||
@@ -72,37 +33,13 @@ const Home: Component<{}> = (p) => {
|
||||
</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 />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* <Summary /> */}
|
||||
<Servers />
|
||||
<Updates />
|
||||
</Grid>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Home;
|
||||
export default Home;
|
||||
|
||||
44
frontend/src/components/home/Tree/Servers.tsx
Normal file
44
frontend/src/components/home/Tree/Servers.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Component, createMemo, createSignal, For } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import Input from "../../shared/Input";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import AddServer from "./AddServer";
|
||||
import Server from "./Server";
|
||||
|
||||
const Servers: Component = () => {
|
||||
const { servers } = useAppState();
|
||||
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 (
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<Input
|
||||
placeholder="filter servers"
|
||||
value={serverFilter()}
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
|
||||
<AddServer />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Servers;
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { Component } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { Operation, Update as UpdateType } from "../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
readableDuration,
|
||||
readableMonitorTimestamp,
|
||||
} from "../../../util/helpers";
|
||||
import { useToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import CenterMenu from "../../shared/menu/CenterMenu";
|
||||
import UpdateMenu from "../../update/UpdateMenu";
|
||||
import s from "./update.module.scss";
|
||||
|
||||
const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
@@ -32,7 +30,6 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
}
|
||||
return p.update.operation.replaceAll("_", " ");
|
||||
};
|
||||
const [showLog, toggleShowLog] = useToggle();
|
||||
return (
|
||||
<Flex
|
||||
class={combineClasses(s.Update, "shadow")}
|
||||
@@ -59,71 +56,7 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
<div>{p.update.operator}</div>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<CenterMenu
|
||||
title={`${operation()} | ${name()}`}
|
||||
show={showLog}
|
||||
toggleShow={toggleShowLog}
|
||||
target={<Icon type="console" />}
|
||||
targetStyle={{ "place-self": "center end" }}
|
||||
targetClass="blue"
|
||||
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" 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>
|
||||
<Show when={log.stdout}>
|
||||
<div>stdout</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stderr ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stdout}
|
||||
</pre>
|
||||
</Show>
|
||||
<Show when={log.stderr}>
|
||||
<div>stderr</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stdout ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stderr}
|
||||
</pre>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Grid>
|
||||
}
|
||||
/>
|
||||
<UpdateMenu update={p.update} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -17,22 +17,4 @@
|
||||
transform-origin: top;
|
||||
animation-name: Enter;
|
||||
animation-duration: 750ms;
|
||||
}
|
||||
|
||||
.LogContainer {
|
||||
max-height: 80vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.Log {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
/* word-wrap: break-word; */
|
||||
tab-size: 2;
|
||||
width: 40rem;
|
||||
max-width: 90vw;
|
||||
box-sizing: border-box;
|
||||
// max-height: 30vh;
|
||||
background-color: rgba(c.$darkgrey, 0.6);
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -17,11 +17,11 @@
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.Tabs {
|
||||
// padding: 1rem;
|
||||
// width: 80%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
// .Tabs {
|
||||
// // padding: 1rem;
|
||||
// // width: 80%;
|
||||
// gap: 0.5rem;
|
||||
// }
|
||||
|
||||
.Container {
|
||||
width: fit-content;
|
||||
|
||||
@@ -1,61 +1,58 @@
|
||||
import { Server, StoredStats } from "@monitor/types";
|
||||
import { Accessor, Component, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { getServerStatsHistory } from "../../../util/query";
|
||||
import Button from "../../util/Button";
|
||||
import Icon from "../../util/Icon";
|
||||
import Grid from "../../util/layout/Grid";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { readableTimestamp } from "../../../util/helpers";
|
||||
import Flex from "../../util/layout/Flex";
|
||||
import Loading from "../../util/loading/Loading";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Loading from "../../shared/loading/Loading";
|
||||
import { ApexOptions } from "apexcharts";
|
||||
import ApexChart from "../../util/ApexChart";
|
||||
import ApexChart from "../../shared/ApexChart";
|
||||
|
||||
const MOVEMENT = 500;
|
||||
const NUM_PTS = 1000;
|
||||
const SKIP = 1;
|
||||
|
||||
const Graphs: Component<{ id: string }> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
const server = () => servers.get(p.id)!;
|
||||
const [stats, setStats] = createSignal<StoredStats[]>();
|
||||
const [offset, setOffset] = createSignal(0);
|
||||
const [reloadingLeft, setReloadingLeft] = createSignal(false);
|
||||
const [reloadingRight, setReloadingRight] = createSignal(false);
|
||||
const [reloadingReset, setReloadingReset] = createSignal(false);
|
||||
const reloadStatsLeft = async () => {
|
||||
setReloadingLeft(true);
|
||||
const newOffset = offset() + MOVEMENT;
|
||||
const stats = await getServerStatsHistory(p.id, newOffset, NUM_PTS, SKIP);
|
||||
setStats(stats.reverse());
|
||||
setOffset(newOffset);
|
||||
setReloadingLeft(false);
|
||||
};
|
||||
const reloadStatsRight = async () => {
|
||||
setReloadingRight(true);
|
||||
const newOffset = Math.max(offset() - MOVEMENT, 0);
|
||||
const stats = await getServerStatsHistory(p.id, newOffset, NUM_PTS, SKIP);
|
||||
setStats(stats.reverse());
|
||||
setOffset(newOffset);
|
||||
setReloadingRight(false);
|
||||
};
|
||||
const reloadStatsReset = async () => {
|
||||
setReloadingReset(true);
|
||||
const stats = await getServerStatsHistory(p.id, 0, NUM_PTS, SKIP);
|
||||
setStats(stats.reverse());
|
||||
setOffset(0);
|
||||
setReloadingReset(false);
|
||||
};
|
||||
getServerStatsHistory(p.id, 0, NUM_PTS, SKIP).then((stats) =>
|
||||
setStats(stats.reverse())
|
||||
);
|
||||
// const { servers } = useAppState();
|
||||
// const server = () => servers.get(p.id)!;
|
||||
// const [stats, setStats] = createSignal<StoredStats[]>();
|
||||
// const [offset, setOffset] = createSignal(0);
|
||||
// const [reloadingLeft, setReloadingLeft] = createSignal(false);
|
||||
// const [reloadingRight, setReloadingRight] = createSignal(false);
|
||||
// const [reloadingReset, setReloadingReset] = createSignal(false);
|
||||
// const reloadStatsLeft = async () => {
|
||||
// setReloadingLeft(true);
|
||||
// const newOffset = offset() + MOVEMENT;
|
||||
// const stats = await getServerStatsHistory(p.id, newOffset, NUM_PTS, SKIP);
|
||||
// setStats(stats.reverse());
|
||||
// setOffset(newOffset);
|
||||
// setReloadingLeft(false);
|
||||
// };
|
||||
// const reloadStatsRight = async () => {
|
||||
// setReloadingRight(true);
|
||||
// const newOffset = Math.max(offset() - MOVEMENT, 0);
|
||||
// const stats = await getServerStatsHistory(p.id, newOffset, NUM_PTS, SKIP);
|
||||
// setStats(stats.reverse());
|
||||
// setOffset(newOffset);
|
||||
// setReloadingRight(false);
|
||||
// };
|
||||
// const reloadStatsReset = async () => {
|
||||
// setReloadingReset(true);
|
||||
// const stats = await getServerStatsHistory(p.id, 0, NUM_PTS, SKIP);
|
||||
// setStats(stats.reverse());
|
||||
// setOffset(0);
|
||||
// setReloadingReset(false);
|
||||
// };
|
||||
// getServerStatsHistory(p.id, 0, NUM_PTS, SKIP).then((stats) =>
|
||||
// setStats(stats.reverse())
|
||||
// );
|
||||
return (
|
||||
<Grid
|
||||
gap="0rem"
|
||||
placeItems="start center"
|
||||
style={{ "background-color": "white" }}
|
||||
>
|
||||
<Show when={stats()}>
|
||||
{/* <Show when={stats()}>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
style={{ margin: "1rem", width: "60%" }}
|
||||
@@ -120,50 +117,50 @@ const Graphs: Component<{ id: string }> = (p) => {
|
||||
<Graph stats={stats} field="cpu" server={server} />
|
||||
<Graph stats={stats} field="mem" server={server} />
|
||||
<Graph stats={stats} field="disk" server={server} />
|
||||
</Show>
|
||||
</Show> */}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const Graph: Component<{
|
||||
stats: Accessor<StoredStats[] | undefined>;
|
||||
field: "cpu" | "mem" | "disk";
|
||||
server: () => Server;
|
||||
// stats: Accessor<StoredStats[] | undefined>;
|
||||
// field: "cpu" | "mem" | "disk";
|
||||
// server: () => Server;
|
||||
}> = (p) => {
|
||||
const options: () => ApexOptions = () => ({
|
||||
chart: {
|
||||
id: "stats",
|
||||
type: "line",
|
||||
width: 800,
|
||||
height: 150,
|
||||
events: {},
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
categories: p.stats()!.map((stat) => readableTimestamp(stat.ts)),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: p.field,
|
||||
data:
|
||||
p.field === "cpu"
|
||||
? p.stats()!.map((stat) => stat.cpu)
|
||||
: p.field === "mem"
|
||||
? p.stats()!.map((stat) => stat.mem.usedMemPercentage)
|
||||
: p.stats()!.map((stat) => stat.disk.usedPercentage),
|
||||
},
|
||||
],
|
||||
// theme: {
|
||||
// mode: isDark() ? "dark" : "light"
|
||||
// },
|
||||
});
|
||||
// const options: () => ApexOptions = () => ({
|
||||
// chart: {
|
||||
// id: "stats",
|
||||
// type: "line",
|
||||
// width: 800,
|
||||
// height: 150,
|
||||
// events: {},
|
||||
// },
|
||||
// xaxis: {
|
||||
// labels: {
|
||||
// show: false,
|
||||
// },
|
||||
// categories: p.stats()!.map((stat) => readableTimestamp(stat.ts)),
|
||||
// },
|
||||
// series: [
|
||||
// {
|
||||
// name: p.field,
|
||||
// data:
|
||||
// p.field === "cpu"
|
||||
// ? p.stats()!.map((stat) => stat.cpu)
|
||||
// : p.field === "mem"
|
||||
// ? p.stats()!.map((stat) => stat.mem.usedMemPercentage)
|
||||
// : p.stats()!.map((stat) => stat.disk.usedPercentage),
|
||||
// },
|
||||
// ],
|
||||
// // theme: {
|
||||
// // mode: isDark() ? "dark" : "light"
|
||||
// // },
|
||||
// });
|
||||
|
||||
return (
|
||||
<Grid placeItems="start center" gap="0rem">
|
||||
<h1 style={{ color: "black" }}>{p.field}</h1>
|
||||
<ApexChart options={options()} />
|
||||
{/* <h1 style={{ color: "black" }}>{p.field}</h1>
|
||||
<ApexChart options={options()} /> */}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Component, lazy } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useToggle } from "../../../util/hooks";
|
||||
import Icon from "../../util/Icon";
|
||||
import CenterMenu from "../../util/menu/CenterMenu";
|
||||
import Icon from "../../shared/Icon";
|
||||
import CenterMenu from "../../shared/menu/CenterMenu";
|
||||
|
||||
const Graphs = lazy(() => import("./Graphs"))
|
||||
|
||||
const StatGraphs: Component<{ id: string }> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
const [show, toggleShow] = useToggle();
|
||||
const name = () => servers.get(p.id)?.name;
|
||||
const name = () => servers.get(p.id)?.server.name;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import Icon from "../../../util/Icon";
|
||||
import Input from "../../../util/Input";
|
||||
import Flex from "../../../util/layout/Flex";
|
||||
import Grid from "../../../util/layout/Grid";
|
||||
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";
|
||||
import { useTheme } from "../../../../state/ThemeProvider";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import Button from "../../../util/Button";
|
||||
|
||||
const ToNotify: Component<{}> = (p) => {
|
||||
const { server, setServer } = useConfig();
|
||||
const { themeClass } = useTheme();
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow", themeClass())}>
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>notify</h1>
|
||||
<Button
|
||||
<button
|
||||
class="green"
|
||||
onClick={() => setServer("toNotify", (toNotify) => [...toNotify, ""])}
|
||||
onClick={() => setServer("to_notify", (toNotify) => toNotify ? [...toNotify, ""] : [""])}
|
||||
>
|
||||
<Icon type="plus" />
|
||||
</Button>
|
||||
</button>
|
||||
</Flex>
|
||||
<For each={server.toNotify}>
|
||||
<For each={server.to_notify}>
|
||||
{(user, index) => (
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<Input
|
||||
placeholder="slack user id"
|
||||
value={user}
|
||||
onEdit={(user) => setServer("toNotify", index(), user)}
|
||||
onEdit={(user) => setServer("to_notify", index(), user)}
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
class="red"
|
||||
onClick={() =>
|
||||
setServer("toNotify", (toNotify) =>
|
||||
toNotify.filter((_, i) => i !== index())
|
||||
setServer("to_notify", (toNotify) =>
|
||||
toNotify!.filter((_, i) => i !== index())
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</Button>
|
||||
</button>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
<Show when={server.toNotify?.length === 0}>
|
||||
<Show when={server.to_notify?.length === 0}>
|
||||
<div>no slack users to notify</div>
|
||||
</Show>
|
||||
</Grid>
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { DockerStat } from "@monitor/types";
|
||||
import { Component, createEffect, createSignal, Show, For } from "solid-js";
|
||||
import { pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { useTheme } from "../../../../state/ThemeProvider";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import { getServerStats } from "../../../../util/query";
|
||||
import Button from "../../../util/Button";
|
||||
import Icon from "../../../util/Icon";
|
||||
import Flex from "../../../util/layout/Flex";
|
||||
import Grid from "../../../util/layout/Grid";
|
||||
import Loading from "../../../util/loading/Loading";
|
||||
import Icon from "../../../shared/Icon";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import s from "./stats.module.scss";
|
||||
|
||||
const DockerStats: Component<{}> = (p) => {
|
||||
const { selected } = useAppState();
|
||||
const [stats, setStats] = createSignal<DockerStat[]>();
|
||||
// const [stats, setStats] = createSignal<DockerStat[]>();
|
||||
const [refreshing, setRefreshing] = createSignal(false);
|
||||
const load = () => {
|
||||
if (selected.id()) {
|
||||
getServerStats(selected.id()).then(setStats);
|
||||
}
|
||||
};
|
||||
createEffect(load);
|
||||
const { themeClass } = useTheme();
|
||||
// const load = () => {
|
||||
// if (selected.id()) {
|
||||
// getServerStats(selected.id()).then(setStats);
|
||||
// }
|
||||
// };
|
||||
// createEffect(load);
|
||||
// const { themeClass } = useTheme();
|
||||
return (
|
||||
<Show
|
||||
when={stats()}
|
||||
when={true}
|
||||
fallback={
|
||||
<Loading
|
||||
type="three-dot"
|
||||
@@ -34,7 +29,7 @@ const DockerStats: Component<{}> = (p) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
{/* <Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>container stats</h1>
|
||||
<Button
|
||||
@@ -69,7 +64,7 @@ const DockerStats: Component<{}> = (p) => {
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from "solid-js";
|
||||
import Grid from "../../../util/layout/Grid";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import DockerStats from "./DockerStats";
|
||||
import Pm2Processes from "./pm2/Pm2Processes";
|
||||
import SystemStats from "./SystemStats";
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
import { Component, createSignal, Show } from "solid-js";
|
||||
import { pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { useTheme } from "../../../../state/ThemeProvider";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import Button from "../../../util/Button";
|
||||
import Icon from "../../../util/Icon";
|
||||
import Flex from "../../../util/layout/Flex";
|
||||
import Grid from "../../../util/layout/Grid";
|
||||
import Loading from "../../../util/loading/Loading";
|
||||
import Icon from "../../../shared/Icon";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import s from "./stats.module.scss";
|
||||
|
||||
const SystemStats: Component<{}> = (p) => {
|
||||
const { selected, servers, serverStats } = useAppState();
|
||||
const { servers, serverStats } = useAppState();
|
||||
const [refreshingStats, setRefreshingStats] = createSignal(false);
|
||||
const sysStats = () => serverStats.get(selected.id(), servers.get(selected.id()));
|
||||
const loadStats = async () => {
|
||||
if (selected.id() && servers.get(selected.id())?.status === "OK") {
|
||||
setRefreshingStats(true);
|
||||
await serverStats.load(selected.id());
|
||||
setRefreshingStats(false);
|
||||
pushNotification("good", "system stats refreshed");
|
||||
}
|
||||
};
|
||||
const { themeClass } = useTheme();
|
||||
// const sysStats = () => serverStats.get(selected.id(), servers.get(selected.id()));
|
||||
// const loadStats = async () => {
|
||||
// if (selected.id() && servers.get(selected.id())?.status === "OK") {
|
||||
// setRefreshingStats(true);
|
||||
// await serverStats.load(selected.id());
|
||||
// setRefreshingStats(false);
|
||||
// pushNotification("good", "system stats refreshed");
|
||||
// }
|
||||
// };
|
||||
return (
|
||||
<Show when={sysStats()}>
|
||||
<Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
<Show when={true}>
|
||||
{/* <Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>system stats</h1>
|
||||
<Button
|
||||
@@ -58,7 +55,7 @@ const SystemStats: Component<{}> = (p) => {
|
||||
gb)
|
||||
</div>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,82 +1,94 @@
|
||||
import { Log as LogType } from "@monitor/types";
|
||||
import {
|
||||
Accessor,
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
} from "solid-js";
|
||||
import { pushNotification } from "../../../../..";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { useTheme } from "../../../../../state/ThemeProvider";
|
||||
import { combineClasses } from "../../../../../util/helpers";
|
||||
import { useToggle } from "../../../../../util/hooks";
|
||||
import { getPm2Log } from "../../../../../util/query";
|
||||
import Button from "../../../../util/Button";
|
||||
import Icon from "../../../../util/Icon";
|
||||
import Grid from "../../../../util/layout/Grid";
|
||||
import CenterMenu from "../../../../util/menu/CenterMenu";
|
||||
import Selector from "../../../../util/menu/Selector";
|
||||
import s from "../stats.module.scss";
|
||||
// import { Log as LogType } from "@monitor/types";
|
||||
// import {
|
||||
// Accessor,
|
||||
// Component,
|
||||
// createEffect,
|
||||
// createSignal,
|
||||
// } from "solid-js";
|
||||
// import { pushNotification } from "../../../../..";
|
||||
// import { useAppState } from "../../../../../state/StateProvider";
|
||||
// import { useTheme } from "../../../../../state/ThemeProvider";
|
||||
// import { combineClasses } from "../../../../../util/helpers";
|
||||
// import { useToggle } from "../../../../../util/hooks";
|
||||
// import { getPm2Log } from "../../../../../util/query";
|
||||
// import Button from "../../../../util/Button";
|
||||
// import Icon from "../../../../util/Icon";
|
||||
// import Grid from "../../../../util/layout/Grid";
|
||||
// import CenterMenu from "../../../../util/menu/CenterMenu";
|
||||
// import Selector from "../../../../util/menu/Selector";
|
||||
// import s from "../stats.module.scss";
|
||||
|
||||
const LogButton: Component<{ name: string }> = (p) => {
|
||||
const { selected } = useAppState();
|
||||
const [show, toggleShow] = useToggle();
|
||||
const [log, setLog] = createSignal<LogType>();
|
||||
const [lines, setLines] = createSignal(50);
|
||||
const load = () => {
|
||||
getPm2Log(selected.id(), p.name, lines()).then((cle) => setLog(cle.log));
|
||||
};
|
||||
// const LogButton: Component<{ name: string }> = (p) => {
|
||||
// const { selected } = useAppState();
|
||||
// const [show, toggleShow] = useToggle();
|
||||
// const [log, setLog] = createSignal<LogType>();
|
||||
// const [lines, setLines] = createSignal(50);
|
||||
// const load = () => {
|
||||
// getPm2Log(selected.id(), p.name, lines()).then((cle) => setLog(cle.log));
|
||||
// };
|
||||
// return (
|
||||
// <CenterMenu
|
||||
// show={show}
|
||||
// toggleShow={toggleShow}
|
||||
// title={`${p.name} log`}
|
||||
// target="show log"
|
||||
// targetClass="blue"
|
||||
// leftOfX={
|
||||
// <>
|
||||
// lines:
|
||||
// <Selector
|
||||
// targetClass="lightgrey"
|
||||
// targetStyle={{ padding: "0.35rem" }}
|
||||
// selected={lines().toString()}
|
||||
// items={["50", "100", "500", "1000"]}
|
||||
// onSelect={(lines) => setLines(Number(lines))}
|
||||
// position="bottom right"
|
||||
// itemStyle={{ width: "4rem" }}
|
||||
// />
|
||||
// <Button class="blue" onClick={async () => {
|
||||
// const cle = await getPm2Log(selected.id(), p.name, lines());
|
||||
// setLog(cle.log);
|
||||
// pushNotification("good", "log reloaded");
|
||||
// }}>
|
||||
// <Icon type="refresh" />
|
||||
// </Button>
|
||||
// </>
|
||||
// }
|
||||
// content={<Log name={p.name} log={log} setLog={setLog} load={load} />}
|
||||
// />
|
||||
// );
|
||||
// };
|
||||
|
||||
// const Log: Component<{
|
||||
// name: string;
|
||||
// log: Accessor<LogType | undefined>;
|
||||
// setLog: (log: LogType) => void;
|
||||
// load: () => void;
|
||||
// }> = (p) => {
|
||||
// createEffect(p.load);
|
||||
// const { themeClass } = useTheme();
|
||||
// return (
|
||||
// <Grid
|
||||
// gap="0.2rem"
|
||||
// style={{ padding: "0.5rem", width: "80vw", height: "90vh" }}
|
||||
// >
|
||||
// <pre class={combineClasses(s.Pm2Log, "scroller", themeClass())}>
|
||||
// {p.log()?.stdout}
|
||||
// </pre>
|
||||
// </Grid>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default LogButton;
|
||||
|
||||
import { Component } from "solid-js";
|
||||
|
||||
const LogButton: Component<{}> = (p) => {
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title={`${p.name} log`}
|
||||
target="show log"
|
||||
targetClass="blue"
|
||||
leftOfX={
|
||||
<>
|
||||
lines:
|
||||
<Selector
|
||||
targetClass="lightgrey"
|
||||
targetStyle={{ padding: "0.35rem" }}
|
||||
selected={lines().toString()}
|
||||
items={["50", "100", "500", "1000"]}
|
||||
onSelect={(lines) => setLines(Number(lines))}
|
||||
position="bottom right"
|
||||
itemStyle={{ width: "4rem" }}
|
||||
/>
|
||||
<Button class="blue" onClick={async () => {
|
||||
const cle = await getPm2Log(selected.id(), p.name, lines());
|
||||
setLog(cle.log);
|
||||
pushNotification("good", "log reloaded");
|
||||
}}>
|
||||
<Icon type="refresh" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
content={<Log name={p.name} log={log} setLog={setLog} load={load} />}
|
||||
/>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const Log: Component<{
|
||||
name: string;
|
||||
log: Accessor<LogType | undefined>;
|
||||
setLog: (log: LogType) => void;
|
||||
load: () => void;
|
||||
}> = (p) => {
|
||||
createEffect(p.load);
|
||||
const { themeClass } = useTheme();
|
||||
return (
|
||||
<Grid
|
||||
gap="0.2rem"
|
||||
style={{ padding: "0.5rem", width: "80vw", height: "90vh" }}
|
||||
>
|
||||
<pre class={combineClasses(s.Pm2Log, "scroller", themeClass())}>
|
||||
{p.log()?.stdout}
|
||||
</pre>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogButton;
|
||||
export default LogButton;
|
||||
@@ -1,115 +1,127 @@
|
||||
import { PM2Process } from "@monitor/types";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { pushNotification } from "../../../../..";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { useTheme } from "../../../../../state/ThemeProvider";
|
||||
import { combineClasses } from "../../../../../util/helpers";
|
||||
import {
|
||||
getPm2Processes,
|
||||
startPm2Process,
|
||||
stopPm2Process,
|
||||
} from "../../../../../util/query";
|
||||
import Button from "../../../../util/Button";
|
||||
import Icon from "../../../../util/Icon";
|
||||
import Flex from "../../../../util/layout/Flex";
|
||||
import Grid from "../../../../util/layout/Grid";
|
||||
import Loading from "../../../../util/loading/Loading";
|
||||
import LogButton from "./Log";
|
||||
import s from "../stats.module.scss";
|
||||
import ConfirmButton from "../../../../util/ConfirmButton";
|
||||
// import { PM2Process } from "@monitor/types";
|
||||
// import {
|
||||
// Component,
|
||||
// createEffect,
|
||||
// createSignal,
|
||||
// For,
|
||||
// Match,
|
||||
// Show,
|
||||
// Switch,
|
||||
// } from "solid-js";
|
||||
// import { pushNotification } from "../../../../..";
|
||||
// import { useAppState } from "../../../../../state/StateProvider";
|
||||
// import { useTheme } from "../../../../../state/ThemeProvider";
|
||||
// import { combineClasses } from "../../../../../util/helpers";
|
||||
// import {
|
||||
// getPm2Processes,
|
||||
// startPm2Process,
|
||||
// stopPm2Process,
|
||||
// } from "../../../../../util/query";
|
||||
// import Button from "../../../../util/Button";
|
||||
// import Icon from "../../../../util/Icon";
|
||||
// import Flex from "../../../../util/layout/Flex";
|
||||
// import Grid from "../../../../util/layout/Grid";
|
||||
// import Loading from "../../../../util/loading/Loading";
|
||||
// import LogButton from "./Log";
|
||||
// import s from "../stats.module.scss";
|
||||
// import ConfirmButton from "../../../../util/ConfirmButton";
|
||||
|
||||
// const Pm2Processes: Component<{}> = (p) => {
|
||||
// const { selected } = useAppState();
|
||||
// const [pm2Proc, setPm2Proc] = createSignal<PM2Process[]>();
|
||||
// const [refreshing, setRefreshing] = createSignal(false);
|
||||
// const loadPm2 = () => {
|
||||
// if (selected.id()) {
|
||||
// try {
|
||||
// getPm2Processes(selected.id()).then(setPm2Proc);
|
||||
// } catch {}
|
||||
// }
|
||||
// };
|
||||
// createEffect(loadPm2);
|
||||
// const { themeClass } = useTheme();
|
||||
// return (
|
||||
// <Show when={pm2Proc() && pm2Proc()!.length > 0}>
|
||||
// <Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
// <Flex justifyContent="space-between" alignItems="center">
|
||||
// <h1>pm2 processes</h1>
|
||||
// <Button
|
||||
// class="blue"
|
||||
// onClick={async () => {
|
||||
// setRefreshing(true);
|
||||
// const processes = await getPm2Processes(selected.id());
|
||||
// setPm2Proc(processes);
|
||||
// setRefreshing(false);
|
||||
// pushNotification("good", "processes refreshed");
|
||||
// }}
|
||||
// >
|
||||
// <Show when={!refreshing()} fallback={<Loading />}>
|
||||
// <Icon type="refresh" />
|
||||
// </Show>
|
||||
// </Button>
|
||||
// </Flex>
|
||||
// <Grid style={{ padding: "0.5rem" }}>
|
||||
// <For each={pm2Proc()}>
|
||||
// {(process) => (
|
||||
// <Flex justifyContent="space-between" alignItems="center">
|
||||
// <h2>{process.name}</h2>
|
||||
// <Flex alignItems="center">
|
||||
// <div>{process.status}</div>
|
||||
// <div>cpu: {process.cpu}%</div>
|
||||
// <div>
|
||||
// mem:{" "}
|
||||
// {process.memory
|
||||
// ? `${process.memory / 1024000} mb`
|
||||
// : "unknown"}
|
||||
// </div>
|
||||
// <Switch>
|
||||
// <Match when={process.status === "online"}>
|
||||
// <ConfirmButton
|
||||
// color="orange"
|
||||
// onConfirm={async () => {
|
||||
// pushNotification("ok", `stopping ${process.name}`);
|
||||
// await stopPm2Process(selected.id(), process.name!);
|
||||
// pushNotification("good", `${process.name} stopped`);
|
||||
// setTimeout(() => loadPm2(), 1000);
|
||||
// }}
|
||||
// >
|
||||
// <Icon type="pause" />
|
||||
// </ConfirmButton>
|
||||
// </Match>
|
||||
// <Match when={process.status === "stopped"}>
|
||||
// <ConfirmButton
|
||||
// color="green"
|
||||
// onConfirm={async () => {
|
||||
// pushNotification("ok", `starting ${process.name}`);
|
||||
// await startPm2Process(selected.id(), process.name!);
|
||||
// pushNotification("good", `${process.name} started`);
|
||||
// setTimeout(() => loadPm2(), 1000);
|
||||
// }}
|
||||
// >
|
||||
// <Icon type="play" />
|
||||
// </ConfirmButton>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// <LogButton name={process.name!} />
|
||||
// </Flex>
|
||||
// </Flex>
|
||||
// )}
|
||||
// </For>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Show>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Pm2Processes;
|
||||
|
||||
import { Component } from "solid-js";
|
||||
|
||||
const Pm2Processes: Component<{}> = (p) => {
|
||||
const { selected } = useAppState();
|
||||
const [pm2Proc, setPm2Proc] = createSignal<PM2Process[]>();
|
||||
const [refreshing, setRefreshing] = createSignal(false);
|
||||
const loadPm2 = () => {
|
||||
if (selected.id()) {
|
||||
try {
|
||||
getPm2Processes(selected.id()).then(setPm2Proc);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
createEffect(loadPm2);
|
||||
const { themeClass } = useTheme();
|
||||
return (
|
||||
<Show when={pm2Proc() && pm2Proc()!.length > 0}>
|
||||
<Grid class={combineClasses(s.StatsContainer, themeClass())}>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<h1>pm2 processes</h1>
|
||||
<Button
|
||||
class="blue"
|
||||
onClick={async () => {
|
||||
setRefreshing(true);
|
||||
const processes = await getPm2Processes(selected.id());
|
||||
setPm2Proc(processes);
|
||||
setRefreshing(false);
|
||||
pushNotification("good", "processes refreshed");
|
||||
}}
|
||||
>
|
||||
<Show when={!refreshing()} fallback={<Loading />}>
|
||||
<Icon type="refresh" />
|
||||
</Show>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Grid style={{ padding: "0.5rem" }}>
|
||||
<For each={pm2Proc()}>
|
||||
{(process) => (
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<h2>{process.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<div>{process.status}</div>
|
||||
<div>cpu: {process.cpu}%</div>
|
||||
<div>
|
||||
mem:{" "}
|
||||
{process.memory
|
||||
? `${process.memory / 1024000} mb`
|
||||
: "unknown"}
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={process.status === "online"}>
|
||||
<ConfirmButton
|
||||
color="orange"
|
||||
onConfirm={async () => {
|
||||
pushNotification("ok", `stopping ${process.name}`);
|
||||
await stopPm2Process(selected.id(), process.name!);
|
||||
pushNotification("good", `${process.name} stopped`);
|
||||
setTimeout(() => loadPm2(), 1000);
|
||||
}}
|
||||
>
|
||||
<Icon type="pause" />
|
||||
</ConfirmButton>
|
||||
</Match>
|
||||
<Match when={process.status === "stopped"}>
|
||||
<ConfirmButton
|
||||
color="green"
|
||||
onConfirm={async () => {
|
||||
pushNotification("ok", `starting ${process.name}`);
|
||||
await startPm2Process(selected.id(), process.name!);
|
||||
pushNotification("good", `${process.name} started`);
|
||||
setTimeout(() => loadPm2(), 1000);
|
||||
}}
|
||||
>
|
||||
<Icon type="play" />
|
||||
</ConfirmButton>
|
||||
</Match>
|
||||
</Switch>
|
||||
<LogButton name={process.name!} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Pm2Processes;
|
||||
export default Pm2Processes;
|
||||
@@ -11,10 +11,6 @@
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.StatsContainer:global(.dark) {
|
||||
background-color: rgba(c.$darkgrey-dark, 0.6);
|
||||
}
|
||||
|
||||
.Stats {
|
||||
padding: 0rem 1rem;
|
||||
}
|
||||
@@ -29,8 +25,4 @@
|
||||
height: 87vh;
|
||||
background-color: rgba(c.$darkgrey, 0.6);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Pm2Log:global(.dark) {
|
||||
background-color: rgba(c.$darkgrey-dark, 0.6);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { Update } from "../../types";
|
||||
import { Update as UpdateType } from "../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
readableDuration,
|
||||
@@ -12,8 +12,9 @@ import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import CenterMenu from "../shared/menu/CenterMenu";
|
||||
import s from "./update.module.scss";
|
||||
import UpdateMenu from "./UpdateMenu";
|
||||
|
||||
const Update: Component<{ update: Update; showName: boolean }> = (p) => {
|
||||
const Update: Component<{ update: UpdateType; showName: boolean }> = (p) => {
|
||||
const { deployments, servers, builds } = useAppState();
|
||||
const name = () => {
|
||||
if (p.update.target.type === "Deployment" && deployments.loaded()) {
|
||||
@@ -60,74 +61,10 @@ const Update: Component<{ update: Update; showName: boolean }> = (p) => {
|
||||
<Icon type="user" />
|
||||
<div>{p.update.operator}</div>
|
||||
</Flex>
|
||||
<CenterMenu
|
||||
title={`${operation()} | ${name()}`}
|
||||
show={showLog}
|
||||
toggleShow={toggleShowLog}
|
||||
target={<Icon type="console" />}
|
||||
targetStyle={{ "place-self": "center end" }}
|
||||
targetClass="blue"
|
||||
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" 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>
|
||||
<Show when={log.stdout}>
|
||||
<div>stdout</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stderr ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stdout}
|
||||
</pre>
|
||||
</Show>
|
||||
<Show when={log.stderr}>
|
||||
<div>stderr</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stdout ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stderr}
|
||||
</pre>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Grid>
|
||||
}
|
||||
/>
|
||||
<UpdateMenu update={p.update} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Update;
|
||||
export default Update;
|
||||
98
frontend/src/components/update/UpdateMenu.tsx
Normal file
98
frontend/src/components/update/UpdateMenu.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { Update as UpdateType } from "../../types";
|
||||
import { combineClasses, readableDuration, readableMonitorTimestamp } from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import Icon from "../shared/Icon";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import CenterMenu from "../shared/menu/CenterMenu";
|
||||
import s from "./update.module.scss";
|
||||
|
||||
const UpdateMenu: Component<{ update: UpdateType }> = (p) => {
|
||||
const { deployments, servers, builds } = 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 operation = () => {
|
||||
return p.update.operation.replaceAll("_", " ");
|
||||
};
|
||||
const [showLog, toggleShowLog] = useToggle();
|
||||
return (
|
||||
<CenterMenu
|
||||
title={`${operation()} | ${name()}`}
|
||||
show={showLog}
|
||||
toggleShow={toggleShowLog}
|
||||
target={<Icon type="console" />}
|
||||
targetStyle={{ "place-self": "center end" }}
|
||||
targetClass="blue"
|
||||
padding="1rem 2rem"
|
||||
content={
|
||||
<Grid class={s.LogContainer} gap="1rem">
|
||||
<Grid gap="0.5rem" class="card light 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 light shadow">
|
||||
<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>
|
||||
<Show when={log.command}>
|
||||
<div>command</div>
|
||||
<pre class={combineClasses(s.Log)}>{log.command}</pre>
|
||||
</Show>
|
||||
<Show when={log.stdout}>
|
||||
<div>stdout</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stderr ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stdout}
|
||||
</pre>
|
||||
</Show>
|
||||
<Show when={log.stderr}>
|
||||
<div>stderr</div>
|
||||
<pre
|
||||
class={combineClasses(s.Log)}
|
||||
// style={{
|
||||
// "max-height": log.stdout ? "30vh" : "60vh",
|
||||
// }}
|
||||
>
|
||||
{log.stderr}
|
||||
</pre>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Grid>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateMenu;
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
.LogContainer {
|
||||
max-height: 80vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.Log {
|
||||
@@ -49,7 +50,6 @@
|
||||
width: 40rem;
|
||||
max-width: 90vw;
|
||||
box-sizing: border-box;
|
||||
max-height: 30vh;
|
||||
background-color: rgba(c.$darkgrey, 0.6);
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -13,6 +13,15 @@
|
||||
"content";
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.app {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"topbar"
|
||||
"content";
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: rgba(c.$lightblue, 0.5);
|
||||
}
|
||||
@@ -20,4 +29,138 @@
|
||||
.card {
|
||||
background-color: c.$grey;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card.light {
|
||||
background-color: c.$lightgrey;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-template-columns: auto 1fr;
|
||||
background-color: c.$darkgrey;
|
||||
}
|
||||
|
||||
.left-content {
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
min-width: 325px;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left-content {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
$anim-time: 500ms;
|
||||
|
||||
.content-enter {
|
||||
grid-area: content;
|
||||
// animation-name: content-enter;
|
||||
// animation-duration: $anim-time;
|
||||
// animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.content-exit {
|
||||
grid-area: content;
|
||||
animation-name: content-exit;
|
||||
animation-duration: $anim-time;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@keyframes content-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-exit {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
background-color: c.$lightgrey;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.updates-container {
|
||||
max-height: calc(225px + 5.5rem);
|
||||
}
|
||||
|
||||
.show-updates-indicator {
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.config {
|
||||
height: 100%;
|
||||
place-items: start center;
|
||||
grid-template-rows: 70vh auto;
|
||||
}
|
||||
|
||||
.config-items {
|
||||
height: fit-content;
|
||||
max-height: 65vh;
|
||||
padding: 0rem 0.5rem;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
width: 450px;
|
||||
flex-wrap: wrap;
|
||||
background-color: c.$lightgrey;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
height: fit-content;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
.config-item {
|
||||
width: calc(100vw - 6rem);
|
||||
}
|
||||
}
|
||||
|
||||
.running {
|
||||
color: c.$textgreen;
|
||||
// text-shadow: 0px 0px 1px c.$textgreen;
|
||||
}
|
||||
|
||||
.exited {
|
||||
color: c.$textred;
|
||||
// text-shadow: 0px 0px 1px c.$textred;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip {
|
||||
background: #f3f3f3;
|
||||
color: black;
|
||||
}
|
||||
@@ -171,10 +171,6 @@ svg {
|
||||
background-color: c.$grey;
|
||||
}
|
||||
|
||||
.lightgrey {
|
||||
background-color: rgba(c.$lightgrey, 0.8);
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: rgba(c.$blue, 0.8);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class Client {
|
||||
}
|
||||
|
||||
get_login_options(): Promise<LoginOptions> {
|
||||
return this.get("/auth/options")
|
||||
return this.get("/auth/options");
|
||||
}
|
||||
|
||||
login_with_github() {
|
||||
@@ -109,7 +109,9 @@ export class Client {
|
||||
|
||||
// deployment
|
||||
|
||||
list_deployments(query?: QueryObject): Promise<DeploymentWithContainerState[]> {
|
||||
list_deployments(
|
||||
query?: QueryObject
|
||||
): Promise<DeploymentWithContainerState[]> {
|
||||
return this.get("/api/deployment/list" + generateQuery(query));
|
||||
}
|
||||
|
||||
@@ -121,6 +123,10 @@ export class Client {
|
||||
return this.get(`/api/deployment/${id}/action_state`);
|
||||
}
|
||||
|
||||
get_deployment_container_log(id: string, tail?: number): Promise<Log> {
|
||||
return this.get(`/api/deployment/${id}/log${generateQuery({ tail })}`);
|
||||
}
|
||||
|
||||
create_deployment(body: CreateDeploymentBody): Promise<Deployment> {
|
||||
return this.post("/api/deployment/create", body);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ use crate::{run_monitor_command, to_monitor_name};
|
||||
use super::docker_login;
|
||||
|
||||
pub async fn container_log(container_name: &str, tail: Option<u64>) -> Log {
|
||||
let tail = match tail {
|
||||
Some(tail) => format!(" --tail {tail}"),
|
||||
None => String::new(),
|
||||
};
|
||||
let command = format!("docker logs {container_name}{tail}");
|
||||
let command = format!(
|
||||
"docker logs {container_name} --tail {}",
|
||||
tail.unwrap_or(1000)
|
||||
);
|
||||
run_monitor_command("get container log", command).await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use monitor_types::{Deployment, DeploymentActionState, DeploymentWithContainerState, Update};
|
||||
use monitor_types::{Deployment, DeploymentActionState, DeploymentWithContainerState, Log, Update};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::MonitorClient;
|
||||
@@ -37,6 +37,19 @@ impl MonitorClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_deployment_container_log(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
tail: Option<u64>,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.get(
|
||||
&format!("/api/deployment/{deployment_id}/log"),
|
||||
json!({ "tail": tail }),
|
||||
)
|
||||
.await
|
||||
.context("failed at get_deployment_container_log")
|
||||
}
|
||||
|
||||
pub async fn create_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
|
||||
@@ -11,6 +11,23 @@ impl PeripheryClient {
|
||||
.context("failed to get container list on periphery")
|
||||
}
|
||||
|
||||
pub async fn container_log(
|
||||
&self,
|
||||
server: &Server,
|
||||
container_name: &str,
|
||||
tail: Option<u64>,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.get_json(
|
||||
server,
|
||||
&format!(
|
||||
"/container/log/{container_name}?tail={}",
|
||||
tail.unwrap_or(50)
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("failed to get container log from periphery")
|
||||
}
|
||||
|
||||
pub async fn container_start(
|
||||
&self,
|
||||
server: &Server,
|
||||
|
||||
@@ -2,6 +2,10 @@ use diff::{Diff, HashMapDiff, OptionDiff, VecDiff};
|
||||
|
||||
use crate::deployment::{DockerRunArgsDiff, RestartModeDiff};
|
||||
|
||||
pub fn f64_diff_no_change(f64_diff: &f64) -> bool {
|
||||
*f64_diff == 0.0
|
||||
}
|
||||
|
||||
pub fn option_diff_no_change<T: Diff>(option_diff: &OptionDiff<T>) -> bool
|
||||
where
|
||||
<T as Diff>::Repr: PartialEq,
|
||||
|
||||
@@ -42,10 +42,15 @@ pub struct Server {
|
||||
pub to_notify: Vec<String>, // slack users to notify
|
||||
|
||||
#[serde(default = "default_cpu_alert")]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "f64_diff_no_change")]))]
|
||||
pub cpu_alert: f64,
|
||||
|
||||
#[serde(default = "default_mem_alert")]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "f64_diff_no_change")]))]
|
||||
pub mem_alert: f64,
|
||||
|
||||
#[serde(default = "default_disk_alert")]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "f64_diff_no_change")]))]
|
||||
pub disk_alert: f64,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
extract::{Path, Query},
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
@@ -18,6 +18,11 @@ struct Container {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetLogQuery {
|
||||
tail: Option<u64>, // default is 1000 if not passed
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
@@ -27,6 +32,15 @@ pub fn router() -> Router {
|
||||
response!(Json(containers))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/log/:name",
|
||||
get(
|
||||
|Path(c): Path<Container>, Query(q): Query<GetLogQuery>| async move {
|
||||
let log = docker::container_log(&c.name, q.tail).await;
|
||||
response!(Json(log))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/stats/:name",
|
||||
get(|Path(c): Path<Container>| async move {
|
||||
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
args:
|
||||
DEPS_INSTALLER: install_full_periphery_deps
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8001:8000"
|
||||
networks:
|
||||
- monitor-network
|
||||
environment:
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
args:
|
||||
DEPS_INSTALLER: install_slim_periphery_deps
|
||||
ports:
|
||||
- "8001:8000"
|
||||
- "8002:8000"
|
||||
networks:
|
||||
- monitor-network
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user