filter updates by operation

This commit is contained in:
beckerinj
2023-01-22 04:29:46 -05:00
parent f5ac23834d
commit 233d9dab33
11 changed files with 239 additions and 104 deletions
+27 -12
View File
@@ -9,6 +9,7 @@ use types::{PermissionLevel, Update, UpdateTarget};
use crate::{
auth::{RequestUser, RequestUserExtension},
helpers::parse_comma_seperated_list,
response,
state::{State, StateExtension},
};
@@ -36,8 +37,18 @@ pub fn router() -> Router {
.unwrap_or_default()
})
.unwrap_or_default();
let operations = value
.get("operations")
.map(|o| {
let o = o.as_str().unwrap_or_default();
if o.len() == 0 {
return None;
}
parse_comma_seperated_list::<String>(o).ok()
})
.flatten();
let updates = state
.list_updates(target, offset, show_builds, &user)
.list_updates(target, offset, show_builds, operations, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(updates))
@@ -92,9 +103,10 @@ impl State {
target: Option<UpdateTarget>,
offset: u64,
show_builds: bool,
operations: Option<Vec<String>>,
user: &RequestUser,
) -> anyhow::Result<Vec<Update>> {
let filter = match target {
let mut filter = match target {
Some(target) => {
if let (UpdateTarget::Deployment(id), true) = (&target, show_builds) {
let deployment = self
@@ -105,32 +117,32 @@ impl State {
.get_build_check_permissions(build_id, user, PermissionLevel::Read)
.await;
if let Ok(_) = build {
Some(doc! {
doc! {
"$or": [
{"target": to_bson(&target).unwrap()},
{"target": { "type": "Build", "id": build_id }, "operation": "build_build"}
],
})
}
} else {
Some(doc! {
doc! {
"target": to_bson(&target).unwrap()
})
}
}
} else {
Some(doc! {
doc! {
"target": to_bson(&target).unwrap()
})
}
}
} else {
self.permission_on_update_target(&target, user).await?;
Some(doc! {
doc! {
"target": to_bson(&target).unwrap()
})
}
}
}
None => {
if user.is_admin {
None
doc! {}
} else {
let permissions_field = format!("permissions.{}", user.id);
let target_filter = doc! {
@@ -184,10 +196,13 @@ impl State {
{ "target.type": "Procedure", "target.id": { "$in": &procedure_ids } }
]
};
Some(filter)
filter
}
}
};
if let Some(operations) = operations {
filter.insert("operation", doc! { "$in": operations });
}
let mut updates = self
.db
.updates
+16
View File
@@ -1,3 +1,6 @@
use std::str::FromStr;
use anyhow::anyhow;
use diff::{Diff, OptionDiff};
#[macro_export]
@@ -25,3 +28,16 @@ where
}
return false;
}
pub fn parse_comma_seperated_list<T: FromStr>(comma_sep_list: &str) -> anyhow::Result<Vec<T>> {
comma_sep_list
.split(",")
.filter(|item| item.len() > 0)
.map(|item| {
let item = item
.parse()
.map_err(|_| anyhow!("error parsing string {item} into type T"))?;
Ok::<T, anyhow::Error>(item)
})
.collect()
}
+12 -2
View File
@@ -101,7 +101,12 @@ impl State {
async fn check_cpu(&self, server: &Server, stats: &SystemStats) {
let server_alert_status = self.server_alert_status.lock().await;
if self.slack.is_none() || server_alert_status.get(&server.id).map(|s| s.cpu_alert).unwrap_or(false) {
if self.slack.is_none()
|| server_alert_status
.get(&server.id)
.map(|s| s.cpu_alert)
.unwrap_or(false)
{
return;
}
drop(server_alert_status);
@@ -175,7 +180,12 @@ impl State {
async fn check_mem(&self, server: &Server, stats: &SystemStats) {
let server_alert_status = self.server_alert_status.lock().await;
if self.slack.is_none() || server_alert_status.get(&server.id).map(|s| s.mem_alert).unwrap_or(false) {
if self.slack.is_none()
|| server_alert_status
.get(&server.id)
.map(|s| s.mem_alert)
.unwrap_or(false)
{
return;
}
drop(server_alert_status);
+1 -1
View File
@@ -23,7 +23,7 @@ const SummaryItem: Component<{
}> = (p) => {
return (
<Flex
class="card light shadow"
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
@@ -1,15 +1,46 @@
import { Component, For, Show } from "solid-js";
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { Operation } from "../../../types";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Loading from "../../shared/loading/Loading";
import Selector from "../../shared/menu/Selector";
import Update from "./Update";
const OPERATIONS = Object.values(Operation)
.filter((e) => e !== "none" && !e.includes("user"))
.map((e) => e.replaceAll("_", " "));
const Updates: Component<{}> = () => {
const { updates } = useAppState();
const [operation, setOperation] = createSignal<Operation>();
createEffect(() => {
if (operation()) {
updates.load([operation()!]);
} else {
updates.load();
}
});
return (
<Grid class="card shadow" style={{ "flex-grow": 1 }}>
<h1>updates</h1>
<Flex alignItems="center" justifyContent="space-between">
<h1>updates</h1>
<Selector
selected={operation() ? operation()! : "all"}
items={["all", ...OPERATIONS]}
onSelect={(o) =>
o === "all"
? setOperation(undefined)
: setOperation(o.replaceAll(" ", "_") as Operation)
}
targetStyle={{ padding: "0" }}
position="bottom right"
searchStyle={{ width: "15rem" }}
menuClass="scroller"
menuStyle={{ "max-height": "50vh" }}
useSearch
/>
</Flex>
<Show
when={updates.loaded()}
fallback={
@@ -22,11 +22,14 @@ const Selector: Component<{
position?: Position;
targetClass?: string;
targetStyle?: JSX.CSSProperties;
menuClass?: string;
menuStyle?: JSX.CSSProperties;
containerStyle?: JSX.CSSProperties;
disabled?: boolean;
disabledClass?: string;
disabledStyle?: JSX.CSSProperties;
useSearch?: boolean;
searchStyle?: JSX.CSSProperties;
itemClass?: string;
itemStyle?: JSX.CSSProperties;
label?: JSXElement;
@@ -35,7 +38,7 @@ const Selector: Component<{
const [search, setSearch] = createSignal("");
let ref: HTMLInputElement | undefined;
createEffect(() => {
if (show()) ref?.focus();
if (show()) setTimeout(() => ref?.focus(), 200);
});
return (
<Show
@@ -69,7 +72,7 @@ const Selector: Component<{
placeholder="search"
value={search()}
onEdit={setSearch}
style={{ "text-align": "end" }}
style={{ "text-align": "end", ...p.searchStyle }}
onKeyDown={(e: any) => {
if (e.key === "Escape") {
toggle();
@@ -104,6 +107,8 @@ const Selector: Component<{
</For>
</>
}
menuClass={p.menuClass}
menuStyle={p.menuStyle}
position={p.position}
padding="0.25rem"
/>
+89 -58
View File
@@ -1,7 +1,12 @@
import { Component, For, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { Operation, Update as UpdateType, UpdateStatus } from "../../types";
import { combineClasses, readableDuration, readableMonitorTimestamp } from "../../util/helpers";
import {
combineClasses,
readableDuration,
readableMonitorTimestamp,
readableVersion,
} from "../../util/helpers";
import { useToggle } from "../../util/hooks";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
@@ -10,7 +15,7 @@ import CenterMenu from "../shared/menu/CenterMenu";
import s from "./update.module.scss";
const UpdateMenu: Component<{ update: UpdateType }> = (p) => {
const { deployments, servers, builds } = useAppState();
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";
@@ -24,7 +29,7 @@ const UpdateMenu: Component<{ update: UpdateType }> = (p) => {
};
const operation = () => {
if (p.update.operation === Operation.BuildBuild) {
return "build"
return "build";
}
return p.update.operation.replaceAll("_", " ");
};
@@ -38,9 +43,7 @@ const UpdateMenu: Component<{ update: UpdateType }> = (p) => {
targetStyle={{ "place-self": "center end" }}
targetClass="blue"
padding="1rem 2rem"
content={
() => <UpdateMenuContent update={p.update} />
}
content={() => <UpdateMenuContent update={p.update} />}
/>
);
};
@@ -50,60 +53,88 @@ export default UpdateMenu;
const UpdateMenuContent: Component<{ update: UpdateType }> = (p) => {
const { usernames } = useAppState();
return (
<Grid class={s.LogContainer} gap="1rem">
<Grid gap="0.5rem" class="card light shadow">
<div>status: {p.update.status.replaceAll("_", " ")}</div>
<div>operator: {usernames.get(p.update.operator)}</div>
<div>started at: {readableMonitorTimestamp(p.update.start_ts)}</div>
<Grid class={s.LogContainer}>
<Grid
gap="0.5rem"
class="card light shadow"
gridTemplateColumns="1fr 1fr"
>
<Flex gap="0.5rem" alignItems="center">
status: <h2>{p.update.status.replaceAll("_", " ")}</h2>
</Flex>
<h2 style={{ "place-self": "center end" }}>
{readableMonitorTimestamp(p.update.start_ts)}
</h2>
<Show when={p.update.end_ts}>
<div>
duration: {readableDuration(p.update.start_ts, p.update.end_ts!)}
</div>
<Flex gap="0.5rem" alignItems="center">
duration:
<h2>{readableDuration(p.update.start_ts, p.update.end_ts!)}</h2>
</Flex>
</Show>
<Flex
gap="0.5rem"
alignItems="center"
style={{ "place-self": "center end" }}
>
<Icon type="user" width="1.25rem" />
<h2>{usernames.get(p.update.operator)}</h2>
</Flex>
<Show when={p.update.version}>
<Flex gap="0.5rem" alignItems="center">
version:
<h2>{readableVersion(p.update.version!)}</h2>
</Flex>
</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>
<UpdateLogs update={p.update} />
</Grid>
);
}
};
const UpdateLogs: Component<{ update: UpdateType }> = (p) => {
return (
<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>
);
};
+14 -6
View File
@@ -1,6 +1,6 @@
import { createEffect, createResource, createSignal } from "solid-js";
import { client } from "..";
import { ServerStatus, SystemInformation, SystemStats, UpdateTarget } from "../types";
import { Operation, ServerStatus, SystemInformation, SystemStats, UpdateTarget } from "../types";
import {
filterOutFromObj,
getNestedEntry,
@@ -152,7 +152,7 @@ export function useDeployments() {
export function useUpdates(target?: UpdateTarget, show_builds?: boolean) {
const updates = useArrayWithId(
() => client.list_updates(0, target, show_builds),
(operations?: Operation[]) => client.list_updates(0, target, show_builds, operations),
["_id", "$oid"]
);
const [noMore, setNoMore] = createSignal(false);
@@ -173,10 +173,13 @@ export function useUpdates(target?: UpdateTarget, show_builds?: boolean) {
};
}
export function useArray<T>(query: () => Promise<T[]>) {
export function useArray<T, O>(query: (options?: O) => Promise<T[]>, options?: O) {
const [collection, set] = createSignal<T[]>();
const load = (options?: O) => {
query(options).then(set);
}
createEffect(() => {
query().then(set);
load(options);
});
const add = (item: T) => {
set((items: T[] | undefined) => (items ? [item, ...items] : [item]));
@@ -186,6 +189,7 @@ export function useArray<T>(query: () => Promise<T[]>) {
};
const loaded = () => (collection() ? true : false);
return {
load,
collection,
add,
addManyToEnd,
@@ -193,10 +197,13 @@ export function useArray<T>(query: () => Promise<T[]>) {
};
}
export function useArrayWithId<T>(query: () => Promise<T[]>, idPath: string[]) {
export function useArrayWithId<T, O>(query: (options?: O) => Promise<T[]>, idPath: string[], options?: O) {
const [collection, set] = createSignal<T[]>();
const load = (options?: O) => {
query(options).then(set);
};
createEffect(() => {
query().then(set);
load(options);
});
const addOrUpdate = (item: T) => {
set((items: T[] | undefined) => {
@@ -226,6 +233,7 @@ export function useArrayWithId<T>(query: () => Promise<T[]>, idPath: string[]) {
};
const loaded = () => (collection() ? true : false);
return {
load,
collection,
addOrUpdate,
addManyToEnd,
+1
View File
@@ -9,6 +9,7 @@
max-width: calc(1200px + 2rem);
box-sizing: border-box;
place-items: center;
margin-bottom: 50vh;
}
@media only screen and (max-width: 1200px) {
+14 -4
View File
@@ -13,6 +13,7 @@ import {
Group,
HistoricalStatsQuery,
Log,
Operation,
Procedure,
Server,
ServerActionState,
@@ -233,7 +234,7 @@ export class Client {
}
get_server_system_info(id: string): Promise<SystemInformation> {
return this.get(`/api/server/${id}/system_information`)
return this.get(`/api/server/${id}/system_information`);
}
create_server(body: CreateServerBody): Promise<Server> {
@@ -314,8 +315,11 @@ 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> {
get_build_versions(
id: string,
query?: BuildVersionsQuery
): Promise<BuildVersionsReponse> {
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
}
@@ -405,13 +409,19 @@ export class Client {
// updates
// show_builds is only relevant for Deployment targets, must pass show_builds = true to include build updates of attached build_id
list_updates(offset: number, target?: UpdateTarget, show_builds?: boolean): Promise<Update[]> {
list_updates(
offset: number,
target?: UpdateTarget,
show_builds?: boolean,
operations?: Operation[]
): Promise<Update[]> {
return this.get(
`/api/update/list${generateQuery({
offset,
type: target && target.type,
id: target && target.id,
show_builds,
operations: operations?.join(","),
})}`
);
}
+25 -17
View File
@@ -1,4 +1,10 @@
import { DockerContainerState, EnvironmentVar, ServerStatus, Timelength, Version } 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(" ");
@@ -48,8 +54,12 @@ export function readableDuration(start_ts: string, end_ts: string) {
const start = new Date(start_ts);
const end = new Date(end_ts);
const durr = end.getTime() - start.getTime();
const seconds = (durr / 1000).toFixed(1);
return `${seconds} seconds`;
const seconds = durr / 1000;
const minutes = Math.floor(seconds / 60);
const remaining_seconds = seconds % 60;
return `${
minutes > 0 ? `${minutes} minute${minutes > 1 ? "s" : ""} ` : ""
}${remaining_seconds.toFixed(minutes > 0 ? 0 : 1)} seconds`;
}
const tzOffset = new Date().getTimezoneOffset() * 60;
@@ -163,9 +173,7 @@ export function parseDotEnvToEnvVars(env: string): EnvironmentVar[] {
.map(([variable, value]) => ({ variable, value }));
}
export function deploymentHeaderStateClass(
state: DockerContainerState,
) {
export function deploymentHeaderStateClass(state: DockerContainerState) {
switch (state) {
case DockerContainerState.Running:
return "running";
@@ -175,28 +183,28 @@ export function deploymentHeaderStateClass(
}
export function version_to_string(version: Version) {
return `${version.major}.${version.minor}.${version.patch}`
return `${version.major}.${version.minor}.${version.patch}`;
}
export function string_to_version(version: string): Version {
const [major, minor, patch] = version.split(".")
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
return 1;
} else if (timelength === Timelength.FiveSeconds) {
return 5
return 5;
} else if (timelength === Timelength.ThirtySeconds) {
return 30
return 30;
} else if (timelength === Timelength.OneMinute) {
return 60
return 60;
}
}
@@ -215,14 +223,14 @@ export function convert_timelength_to_ms(timelength: Timelength) {
export function readableStorageAmount(gb: number) {
if (gb > 512) {
return `${(gb / 1024).toFixed(1)} TB`
return `${(gb / 1024).toFixed(1)} TB`;
} else if (gb < 1) {
return `${(gb * 1024).toFixed()} MiB`
return `${(gb * 1024).toFixed()} MiB`;
} else {
return `${gb.toFixed()} GiB`
return `${gb.toFixed()} GiB`;
}
}
export function readableVersion(version: Version) {
return `v${version.major}.${version.minor}.${version.patch}`;
}
}