mirror of
https://github.com/moghtech/komodo.git
synced 2026-04-28 11:49:39 -05:00
build stats card
This commit is contained in:
69
frontend/src/components/home/BuildSummary.tsx
Normal file
69
frontend/src/components/home/BuildSummary.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user