implement home, servers, implement container log passthrough

This commit is contained in:
mbecker20
2023-01-02 05:28:38 +00:00
parent 3006aa7fa3
commit 8b16aff3bf
30 changed files with 765 additions and 611 deletions

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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(

View File

@@ -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;

View 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;

View File

@@ -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>
);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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";

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View 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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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")]

View File

@@ -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 {

View File

@@ -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: