build stats card

This commit is contained in:
mbecker20
2023-04-21 08:08:15 +00:00
parent c4f45e05f1
commit 4c8f96a30f
10 changed files with 188 additions and 19 deletions

View File

@@ -0,0 +1,69 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart from "../shared/LightweightChart";
import Loading from "../shared/loading/Loading";
import { COLORS } from "../../style/colors";
const BuildSummary: Component<{}> = (p) => {
const { build_stats } = useAppState();
return (
<Grid class="full-size card" gridTemplateRows="auto 1fr">
<Flex
justifyContent="space-between"
alignItems="center"
style={{ height: "fit-content" }}
>
<h2>last 30 days</h2>
<Flex>
<Flex alignItems="center" gap="0.5rem">
<div class="dimmed">build time: </div>
<h2>{build_stats.get()?.total_time.toFixed(1)} hrs</h2>
</Flex>
<Flex alignItems="center" gap="0.5rem">
<div class="dimmed">build count: </div>
<h2>{build_stats.get()?.total_count.toFixed()}</h2>
</Flex>
</Flex>
</Flex>
<Show
when={build_stats.get()}
fallback={
<Grid class="full-size" placeItems="center">
<Loading type="three-dot" />
</Grid>
}
>
<LightweightChart
areas={[
{
line: build_stats.get()!.days.map((day) => ({
value: day.count,
time: day.ts / 1000,
})),
// color: COLORS.blue,
lineColor: COLORS.blue,
topColor: `${COLORS.blue}B3`,
bottomColor: `${COLORS.blue}0D`,
priceLineVisible: false,
priceFormat: {
minMove: 1,
},
},
]}
timeVisible={false}
options={{
grid: {
horzLines: { visible: false },
vertLines: { visible: false },
},
}}
disableScroll
/>
</Show>
</Grid>
);
};
export default BuildSummary;

View File

@@ -1,30 +1,44 @@
import {
Component,
Component, Match, Show, Switch,
} from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import Grid from "../shared/layout/Grid";
import SimpleTabs from "../shared/tabs/SimpleTabs";
import { ControlledSimpleTabs } from "../shared/tabs/SimpleTabs";
import Summary from "./Summary";
import Builds from "./Tree/Builds";
import Groups from "./Tree/Groups";
import { TreeProvider } from "./Tree/Provider";
import Updates from "./Updates/Updates";
import { useLocalStorage } from "../../util/hooks";
import BuildSummary from "./BuildSummary";
const Home: Component<{}> = (p) => {
const { isSemiMobile } = useAppDimensions();
const [selectedTab, setTab] = useLocalStorage<"servers" | "builds">(
"servers",
"home-groups-servers-tab-v2"
);
return (
<>
<Grid
style={{ width: "100%" }}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<Summary />
<Switch>
<Match when={selectedTab() === "servers"}>
<Summary />
</Match>
<Match when={selectedTab() === "builds"}>
<BuildSummary />
</Match>
</Switch>
<Updates />
</Grid>
<TreeProvider>
<SimpleTabs
<ControlledSimpleTabs
selected={selectedTab}
set={setTab as any}
containerStyle={{ width: "100%" }}
localStorageKey="home-groups-servers-tab-v1"
tabs={[
{
title: "servers",
@@ -32,8 +46,8 @@ const Home: Component<{}> = (p) => {
},
{
title: "builds",
element: () => <Builds />
}
element: () => <Builds />,
},
]}
/>
</TreeProvider>

View File

@@ -1,7 +1,11 @@
import {
AreaSeriesPartialOptions,
BarSeriesPartialOptions,
ChartOptions,
ColorType,
createChart,
DeepPartial,
HistogramSeriesPartialOptions,
IChartApi,
ISeriesApi,
LineSeriesPartialOptions,
@@ -28,9 +32,14 @@ export type LightweightArea = {
line: LightweightValue[];
} & AreaSeriesPartialOptions;
export type LightweightHistogram = {
line: LightweightValue[];
} & HistogramSeriesPartialOptions;
const LightweightChart: Component<{
lines?: LightweightLine[];
areas?: LightweightArea[];
histograms?: LightweightHistogram[];
class?: string;
style?: JSX.CSSProperties;
width?: string;
@@ -38,11 +47,15 @@ const LightweightChart: Component<{
disableScroll?: boolean;
onCreateLineSeries?: (series: ISeriesApi<"Line">) => void;
onCreateAreaSeries?: (series: ISeriesApi<"Area">) => void;
onCreateHistogramSeries?: (series: ISeriesApi<"Histogram">) => void;
timeVisible?: boolean;
options?: DeepPartial<ChartOptions>;
}> = (p) => {
let el: HTMLDivElement;
const [chart, setChart] = createSignal<IChartApi>();
let lineSeries: ISeriesApi<"Line">[] = [];
let areaSeries: ISeriesApi<"Area">[] = [];
let histogramSeries: ISeriesApi<"Histogram">[] = [];
const [loaded, setLoaded] = createSignal(false);
onMount(() => {
if (loaded()) return;
@@ -58,9 +71,10 @@ const LightweightChart: Component<{
horzLines: { color: "#3f454d" },
vertLines: { color: "#3f454d" },
},
timeScale: { timeVisible: true },
timeScale: { timeVisible: p.timeVisible ?? true },
handleScroll: p.disableScroll ? false : true,
handleScale: p.disableScroll ? false : true,
...p.options
});
chart.timeScale().fitContent();
setChart(chart);
@@ -95,6 +109,20 @@ const LightweightChart: Component<{
});
areaSeries = series;
}
for (const series of histogramSeries) {
chart()!.removeSeries(series);
}
if (p.histograms) {
const series = p.histograms.map((line) => {
const series = chart()!.addHistogramSeries(line);
series.setData(line.line as any);
if (p.onCreateHistogramSeries) {
p.onCreateHistogramSeries(series);
}
return series;
});
histogramSeries = series;
}
chart()!.timeScale().fitContent();
}
});

View File

@@ -28,19 +28,19 @@ const SimpleTabs: Component<{
containerClass?: string;
containerStyle?: JSX.CSSProperties;
}> = (p) => {
const def = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
const defaultSelected = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
const [selected, set] = p.localStorageKey
? useLocalStorage(def, p.localStorageKey)
: createSignal(def);
? useLocalStorage(defaultSelected, p.localStorageKey)
: createSignal(defaultSelected);
createEffect(() => {
if (p.tabs.filter((tab) => tab.title === selected())[0] === undefined) {
set(p.tabs[0].title);
}
});
return <ControlledTabs selected={selected} set={set} {...p} />;
return <ControlledSimpleTabs selected={selected} set={set} {...p} />;
};
export const ControlledTabs: Component<{
export const ControlledSimpleTabs: Component<{
tabs: Tab[];
selected: Accessor<string>;
set: LocalStorageSetter<string>;

View File

@@ -12,7 +12,7 @@ import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../util/helpers";
import { useLocalStorage, useLocalStorageToggle } from "../../util/hooks";
import { useLocalStorageToggle } from "../../util/hooks";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";

View File

@@ -3,6 +3,7 @@ import { createContext, createResource, ParentComponent, Resource, useContext }
import { useWindowKeyDown } from "../util/hooks";
import {
useBuilds,
useBuildStats,
useDeployments,
useGroups,
useProcedures,
@@ -19,6 +20,7 @@ import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { AwsBuilderConfig, PermissionLevel, UpdateTarget } from "../types";
import { client } from "..";
import { BuildStatsResponse } from "../util/client_types";
export type State = {
usernames: ReturnType<typeof useUsernames>;
@@ -43,6 +45,7 @@ export type State = {
docker_organizations: Resource<string[]>;
github_webhook_base_url: Resource<string>;
name_from_update_target: (target: UpdateTarget) => string;
build_stats: ReturnType<typeof useBuildStats>;
};
const context = createContext<
@@ -93,6 +96,7 @@ export const AppStateProvider: ParentComponent = (p) => {
});
},
builds,
build_stats: useBuildStats(),
getPermissionOnBuild: (id: string) => {
const build = builds.get(id)!;
const permissions = build.permissions![userId] as
@@ -161,7 +165,7 @@ export const AppStateProvider: ParentComponent = (p) => {
} else {
return "unknown"
}
}
},
};
// createEffect(() => {

View File

@@ -13,6 +13,7 @@ import {
intoCollection,
keepOnlyInObj,
} from "../util/helpers";
import { BuildStatsResponse } from "../util/client_types";
type Collection<T> = Record<string, T>;
@@ -245,6 +246,26 @@ export function useBuilds() {
);
}
let build_stats_loading = false;
export function useBuildStats() {
const [stats, set] = createSignal<BuildStatsResponse>();
const reload = () => {
client.get_build_stats().then(set);
};
const get = () => {
if (stats()) {
return stats();
} else if (!build_stats_loading) {
build_stats_loading = true;
reload()
}
}
return {
get,
reload,
};
}
const deploymentIdPath = ["deployment", "_id", "$oid"];
export function useDeployments() {

View File

@@ -63,7 +63,15 @@ function connectToWs(state: State) {
}
async function handleMessage(
{ deployments, builds, servers, groups, procedures, updates }: State,
{
deployments,
builds,
servers,
groups,
procedures,
updates,
build_stats,
}: State,
update: Update
) {
updates.addOrUpdate(update);
@@ -135,13 +143,16 @@ async function handleMessage(
if (update.status === UpdateStatus.Complete) {
builds.delete(update.target.id!);
}
} else if (
[Operation.UpdateBuild, Operation.BuildBuild].includes(update.operation)
) {
} else if (update.operation === Operation.UpdateBuild) {
if (update.status === UpdateStatus.Complete) {
const build = await client.get_build(update.target.id!);
builds.update(build);
}
} else if (update.operation === Operation.BuildBuild) {
if (update.status === UpdateStatus.Complete) {
build_stats.reload();
client.get_build(update.target.id!).then((build) => builds.update(build));
}
}
// server

View File

@@ -29,6 +29,8 @@ import {
UserCredentials,
} from "../types";
import {
BuildStatsQuery,
BuildStatsResponse,
BuildVersionsQuery,
CopyBuildBody,
CopyDeploymentBody,
@@ -401,6 +403,10 @@ export class Client {
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
}
get_build_stats(query?: BuildStatsQuery): Promise<BuildStatsResponse> {
return this.get(`/api/build/stats${generateQuery(query as any)}`);
}
create_build(body: CreateBuildBody): Promise<Build> {
return this.post("/api/build/create", body);
}

View File

@@ -19,6 +19,22 @@ export interface BuildVersionsQuery {
patch?: number;
}
export interface BuildStatsQuery {
page?: number;
}
export interface BuildStatsResponse {
total_time: number;
total_count: number;
days: BuildStatsDay[];
}
export interface BuildStatsDay {
time: number;
count: number;
ts: number;
}
export interface CreateDeploymentBody {
name: string;
server_id: string;