select build version for monitor build

This commit is contained in:
mbecker20
2023-01-08 06:11:34 +00:00
parent 2e5f2d11b4
commit e18cd2eebb
15 changed files with 324 additions and 59 deletions
+82 -2
View File
@@ -5,10 +5,15 @@ use axum::{
Extension, Json, Router,
};
use helpers::handle_anyhow_error;
use mungos::{Deserialize, Document, Serialize};
use types::{traits::Permissioned, Build, BuildActionState, PermissionLevel};
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
use types::{
traits::Permissioned, Build, BuildActionState, BuildVersionsReponse, Operation,
PermissionLevel, UpdateStatus,
};
use typeshare::typeshare;
const NUM_VERSIONS_PER_PAGE: u64 = 10;
use crate::{
auth::{RequestUser, RequestUserExtension},
response,
@@ -36,6 +41,16 @@ struct CopyBuildBody {
server_id: String,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
pub struct BuildVersionsQuery {
#[serde(default)]
page: u32,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
}
pub fn router() -> Router {
Router::new()
.route(
@@ -197,6 +212,21 @@ pub fn router() -> Router {
},
),
)
.route(
"/:id/versions",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(BuildId { id }),
Query(query): Query<BuildVersionsQuery>| async move {
let versions = state
.get_build_versions(&id, &user, query)
.await
.map_err(handle_anyhow_error)?;
response!(Json(versions))
},
),
)
}
impl State {
@@ -240,4 +270,54 @@ impl State {
.clone();
Ok(action_state)
}
pub async fn get_build_versions(
&self,
id: &str,
user: &RequestUser,
query: BuildVersionsQuery,
) -> anyhow::Result<Vec<BuildVersionsReponse>> {
self.get_build_check_permissions(&id, user, PermissionLevel::Read)
.await?;
let mut filter = doc! {
"target": {
"type": "Build",
"id": id
},
"operation": Operation::BuildBuild.to_string(),
"status": UpdateStatus::Complete.to_string(),
"success": true
};
if let Some(major) = query.major {
filter.insert("version.major", major);
}
if let Some(minor) = query.minor {
filter.insert("version.minor", minor);
}
if let Some(patch) = query.patch {
filter.insert("version.patch", patch);
}
let versions = self
.db
.updates
.get_some(
filter,
FindOptions::builder()
.sort(doc! { "_id": -1 })
.limit(NUM_VERSIONS_PER_PAGE as i64)
.skip(query.page as u64 * NUM_VERSIONS_PER_PAGE)
.build(),
)
.await
.context("failed to pull versions from mongo")?
.into_iter()
.map(|u| (u.version, u.start_ts))
.filter(|(v, _)| v.is_some())
.map(|(v, ts)| BuildVersionsReponse {
version: v.unwrap(),
ts,
})
.collect();
Ok(versions)
}
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
<g id="chevron_right_1_">
<g>
<path fill-rule="evenodd" fill="#fceade" clip-rule="evenodd" d="M13.71,9.29l-6-6C7.53,3.11,7.28,3,7,3C6.45,3,6,3.45,6,4
c0,0.28,0.11,0.53,0.29,0.71L11.59,10l-5.29,5.29C6.11,15.47,6,15.72,6,16c0,0.55,0.45,1,1,1c0.28,0,0.53-0.11,0.71-0.29l6-6
C13.89,10.53,14,10.28,14,10C14,9.72,13.89,9.47,13.71,9.29z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 698 B

@@ -9,7 +9,7 @@ import {
useContext,
} from "solid-js";
import { createStore, SetStoreFunction } from "solid-js/store";
import { client } from "../../../..";
import { client, pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { Deployment, Operation, PermissionLevel } from "../../../../types";
@@ -95,7 +95,11 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
const save = () => {
setDeployment("updating", true);
client.update_deployment(deployment);
client.update_deployment(deployment).catch(e => {
console.error(e);
pushNotification("bad", "update deployment failed");
setDeployment("updating", false);
});
};
let update_unsub = () => {};
@@ -1,6 +1,8 @@
import { Component, Show } from "solid-js";
import { Component, createEffect, createSignal, Show } from "solid-js";
import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import { combineClasses } from "../../../../../util/helpers";
import { BuildVersionsReponse } from "../../../../../types";
import { combineClasses, string_to_version, version_to_string } from "../../../../../util/helpers";
import Input from "../../../../shared/Input";
import Flex from "../../../../shared/layout/Flex";
import Selector from "../../../../shared/menu/Selector";
@@ -9,6 +11,12 @@ import { useConfig } from "../Provider";
const Image: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const { builds } = useAppState();
const [versions, setVersions] = createSignal<BuildVersionsReponse[]>([]);
createEffect(() => {
if (deployment.build_id) {
client.get_build_versions(deployment.build_id).then(setVersions);
}
});
return (
<Flex
class={combineClasses("config-item shadow")}
@@ -48,14 +56,39 @@ const Image: Component<{}> = (p) => {
? undefined
: builds.ids()!.find((id) => builds.get(id)?.name === build)
);
setDeployment(
"docker_run_args", { image: undefined }
);
setDeployment("docker_run_args", { image: "" });
}}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
<Show when={deployment.build_id}>
<Selector
targetClass="blue"
selected={
deployment.build_version
? `v${version_to_string(deployment.build_version)}`
: "latest"
}
items={[
"latest",
...versions().map((v) => `v${version_to_string(v.version)}`),
]}
onSelect={(version) => {
if (version === "latest") {
setDeployment("build_version", undefined);
} else {
setDeployment(
"build_version",
string_to_version(version.replace("v", ""))
);
}
}}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
</Show>
</Show>
</Flex>
</Flex>
@@ -3,14 +3,17 @@ import {
Accessor,
Component,
createEffect,
createMemo,
createSignal,
Show,
} from "solid-js";
import { client } from "../../../..";
import { SystemStats, SystemStatsRecord, Timelength } from "../../../../types";
import { convertTsMsToLocalUnixTsInSecs } from "../../../../util/helpers";
import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../../../util/helpers";
import { useLocalStorage } from "../../../../util/hooks";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import LightweightChart from "../../../shared/LightweightChart";
@@ -24,6 +27,7 @@ const TIMELENGTHS = [
Timelength.FifteenMinutes,
Timelength.OneHour,
Timelength.SixHours,
Timelength.TwelveHours,
Timelength.OneDay,
];
@@ -34,17 +38,30 @@ const Stats: Component<{}> = (p) => {
"server-stats-timelength-v3"
);
const [currStats, setCurrStats] = createSignal<SystemStats>();
const [loadingCurr, setLoadingCurr] = createSignal(false);
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
const [page, setPage] = createSignal(0);
const load_curr_stats = () => {
setLoadingCurr(true);
client.get_server_stats(params.id).then((stats) => {
setCurrStats(stats);
setLoadingCurr(false);
});
};
createEffect(() => {
client.get_server_stats(params.id).then(setCurrStats);
client
.get_server_stats_history(params.id, {
interval: timelength(),
page: page(),
limit: 1000,
networks: true,
components: true,
})
.then(setStats);
});
createEffect(() => {
load_curr_stats();
})
// createEffect(() => console.log(stats()))
return (
<Grid
@@ -58,7 +75,7 @@ const Stats: Component<{}> = (p) => {
<Flex
style={{ width: "100%" }}
alignItems="center"
justifyContent="space-between"
// justifyContent="space-between"
>
<Flex class="card light shadow" alignItems="center">
<Show when={currStats()} fallback={<Loading type="three-dot" />}>
@@ -66,7 +83,8 @@ const Stats: Component<{}> = (p) => {
cpu: <h2>{currStats()!.cpu_perc.toFixed(1)}%</h2>
</Grid>
<Grid gap="0" placeItems="start center">
mem: {currStats()!.mem_total_gb.toFixed(1)} GB
mem:
<div>{currStats()!.mem_total_gb.toFixed(1)} GB</div>
<h2>
{(
(100 * currStats()!.mem_used_gb) /
@@ -76,7 +94,8 @@ const Stats: Component<{}> = (p) => {
</h2>
</Grid>
<Grid gap="0" placeItems="start center">
disk: {currStats()!.disk.total_gb.toFixed(1)} GB
disk:
<div>{currStats()!.disk.total_gb.toFixed(1)} GB</div>
<h2>
{(
(100 * currStats()!.disk.used_gb) /
@@ -85,8 +104,31 @@ const Stats: Component<{}> = (p) => {
% full
</h2>
</Grid>
<button class="blue" onClick={load_curr_stats}>
<Show when={!loadingCurr()} fallback={<Loading />}>
<Icon type="refresh" />
</Show>
</button>
</Show>
</Flex>
<Flex class="card light shadow" alignItems="center">
<button class="darkgrey" onClick={() => {
setPage(page => page + 1);
}}>
<Icon type="chevron-left" />
</button>
<button class="darkgrey" onClick={() => {
setPage(page => page > 0 ? page - 1 : 0);
}}>
<Icon type="chevron-right" />
</button>
<button class="darkgrey" onClick={() => {
setPage(0)
}}>
<Icon type="double-chevron-right" />
</button>
<div>page: {page() + 1}</div>
</Flex>
<Selector
selected={timelength()}
items={TIMELENGTHS}
@@ -132,7 +174,7 @@ const CpuChart: Component<{
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
lines={() => [{ title: "%", color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
@@ -168,7 +210,7 @@ const MemChart: Component<{
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
lines={() => [{ title: selected(), color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
@@ -204,7 +246,7 @@ const DiskChart: Component<{
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
lines={() => [{ title: selected(), color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
@@ -218,7 +260,11 @@ const NetworkChart: Component<{
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value: s.networks.map((n) => n.recieved_kb).reduce((p, c) => p + c),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.recieved_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
@@ -226,7 +272,11 @@ const NetworkChart: Component<{
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value: s.networks.map((n) => n.transmitted_kb).reduce((p, c) => p + c),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.transmitted_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
@@ -234,14 +284,14 @@ const NetworkChart: Component<{
<Show when={recv_line()}>
<Grid gap="0" class="card dark shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>network kb</h2>
<h2>network kb/s</h2>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [
{ color: "#41764c", line: recv_line()! },
{ color: "#952E23", line: trans_line()! },
{ title: "recv", color: "#41764c", line: recv_line()! },
{ title: "send", color: "#952E23", line: trans_line()! },
]}
/>
</Grid>
+1
View File
@@ -14,6 +14,7 @@ export type IconType =
| "star-empty"
| "star"
| "chevron-left"
| "chevron-right"
| "trash"
| "info-sign"
| "menu"
@@ -1,17 +1,35 @@
import { ColorType, createChart, IChartApi, ISeriesApi } from "lightweight-charts";
import { Component, createEffect, createSignal, JSX, onCleanup, onMount } from "solid-js";
import {
ColorType,
createChart,
IChartApi,
ISeriesApi,
} from "lightweight-charts";
import {
Component,
createEffect,
createSignal,
JSX,
onCleanup,
onMount,
} from "solid-js";
type LinesData = {
color: string,
line: LineDataPoint[]
}
title: string;
color: string;
priceLineVisible?: boolean;
line: LineDataPoint[];
};
type LineDataPoint = {
time: number;
value: number;
value: number;
};
const LightweightChart: Component<{ style?: JSX.CSSProperties, class?: string, lines?: () => LinesData[] }> = (p) => {
const LightweightChart: Component<{
style?: JSX.CSSProperties;
class?: string;
lines?: () => LinesData[];
}> = (p) => {
let el: HTMLDivElement;
const [chart, setChart] = createSignal<IChartApi>();
let lineSeries: ISeriesApi<"Line">[] = [];
@@ -41,24 +59,34 @@ const LightweightChart: Component<{ style?: JSX.CSSProperties, class?: string, l
chart()!.removeSeries(series);
}
const series = p.lines().map((line) => {
const series = chart()!.addLineSeries({ color: line.color });
const series = chart()!.addLineSeries({
color: line.color,
title: line.title,
priceLineVisible: line.priceLineVisible || false
});
series.setData(line.line as any);
return series;
});
lineSeries = series;
}
})
});
const handleResize = () => {
if (el && chart()) {
chart()!.applyOptions({ width: el.clientWidth });
}
};
addEventListener("resize", handleResize);
onCleanup(() => {
chart()?.remove();
removeEventListener("resize", handleResize);
})
return <div ref={el!} class={p.class} style={{ width: "100%", height: "100%", ...p.style }} />;
addEventListener("resize", handleResize);
onCleanup(() => {
chart()?.remove();
removeEventListener("resize", handleResize);
});
return (
<div
ref={el!}
class={p.class}
style={{ width: "100%", height: "100%", ...p.style }}
/>
);
};
export default LightweightChart;
+8 -3
View File
@@ -40,6 +40,11 @@ export interface DockerBuildArgs {
build_args?: EnvironmentVar[];
}
export interface BuildVersionsReponse {
version: Version;
ts: string;
}
export interface Deployment {
_id?: string;
name: string;
@@ -245,9 +250,9 @@ export interface SystemStatsRecord {
mem_used_gb: number;
mem_total_gb: number;
disk: DiskUsage;
networks: SystemNetwork[];
components: SystemComponent[];
processes: SystemProcess[];
networks?: SystemNetwork[];
components?: SystemComponent[];
processes?: SystemProcess[];
polling_rate: Timelength;
}
+12 -2
View File
@@ -5,6 +5,7 @@ import {
BasicContainerInfo,
Build,
BuildActionState,
BuildVersionsReponse,
Deployment,
DeploymentActionState,
DeploymentWithContainerState,
@@ -25,6 +26,7 @@ import {
UserCredentials,
} from "../types";
import {
BuildVersionsQuery,
CopyBuildBody,
CopyDeploymentBody,
CreateBuildBody,
@@ -241,8 +243,13 @@ export class Client {
return this.patch("/api/server/update", server);
}
get_server_stats(server_id: string, query?: SystemStatsQuery): Promise<SystemStats> {
return this.get(`/api/server/${server_id}/stats${generateQuery(query as any)}`);
get_server_stats(
server_id: string,
query?: SystemStatsQuery
): Promise<SystemStats> {
return this.get(
`/api/server/${server_id}/stats${generateQuery(query as any)}`
);
}
get_server_stats_history(
@@ -298,6 +305,9 @@ export class Client {
get_build_action_state(id: string): Promise<BuildActionState> {
return this.get(`/api/build/${id}/action_state`);
}
get_build_versions(id: string, query?: BuildVersionsQuery): Promise<BuildVersionsReponse> {
return this.get(`/api/build/${id}/versions${generateQuery(query)}`);
}
create_build(body: CreateBuildBody): Promise<Build> {
return this.post("/api/build/create", body);
+7
View File
@@ -14,6 +14,13 @@ export interface CopyBuildBody {
server_id: string;
}
export interface BuildVersionsQuery {
page?: number;
major?: number;
minor?: number;
patch?: number;
}
export interface CreateDeploymentBody {
name: string;
server_id: string;
+27 -1
View File
@@ -1,4 +1,4 @@
import { DockerContainerState, EnvironmentVar, ServerStatus } from "../types";
import { DockerContainerState, EnvironmentVar, ServerStatus, Timelength, Version } from "../types";
export function combineClasses(...classes: (string | false | undefined)[]) {
return classes.filter((c) => (c ? true : false)).join(" ");
@@ -173,3 +173,29 @@ export function deploymentHeaderStateClass(
return "exited";
}
}
export function version_to_string(version: Version) {
return `${version.major}.${version.minor}.${version.patch}`
}
export function string_to_version(version: string): Version {
const [major, minor, patch] = version.split(".")
return {
major: Number(major),
minor: Number(minor),
patch: Number(patch),
}
}
export function get_to_one_sec_divisor(timelength: Timelength) {
// returns what the timelength needs to be divided to convert to per second values
if (timelength === Timelength.OneSecond) {
return 1
} else if (timelength === Timelength.FiveSeconds) {
return 5
} else if (timelength === Timelength.ThirtySeconds) {
return 30
} else if (timelength === Timelength.OneMinute) {
return 60
}
}
+16 -1
View File
@@ -1,5 +1,5 @@
use anyhow::Context;
use monitor_types::{Build, BuildActionState, Update};
use monitor_types::{Build, BuildActionState, BuildVersionsReponse, Update};
use serde_json::{json, Value};
use crate::MonitorClient;
@@ -24,6 +24,21 @@ impl MonitorClient {
.await
}
pub async fn get_build_versions(
&self,
build_id: &str,
page: u32,
major: impl Into<Option<u32>>,
minor: impl Into<Option<u32>>,
patch: impl Into<Option<u32>>,
) -> anyhow::Result<BuildVersionsReponse> {
self.get(
&format!("/api/build/{build_id}/versions"),
json!({ "page": page, "major": major.into(), "minor": minor.into(), "patch": patch.into() }),
)
.await
}
pub async fn create_build(&self, name: &str, server_id: &str) -> anyhow::Result<Build> {
self.post(
"/api/build/create",
+8 -4
View File
@@ -50,19 +50,16 @@ pub struct Build {
pub on_clone: Option<Command>,
// build related
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub pre_build: Option<Command>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub docker_build_args: Option<DockerBuildArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub docker_account: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
#[serde(default)]
#[diff(attr(#[serde(skip)]))]
#[builder(setter(skip))]
pub last_built_at: String,
@@ -115,3 +112,10 @@ pub struct DockerBuildArgs {
#[serde(default)]
pub build_args: Vec<EnvironmentVar>,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildVersionsReponse {
pub version: Version,
pub ts: String,
}
-8
View File
@@ -35,36 +35,28 @@ pub struct Deployment {
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
pub docker_run_args: DockerRunArgs,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub build_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub build_version: Option<Version>,
// deployment repo related
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub repo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub github_account: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub on_clone: Option<Command>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub on_pull: Option<Command>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub repo_mount: Option<Conversion>,
-2
View File
@@ -57,11 +57,9 @@ pub struct Server {
#[diff(attr(#[serde(skip_serializing_if = "timelength_diff_no_change")]))]
pub stats_interval: Timelength,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub instance_id: Option<String>,