Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
449 changes: 416 additions & 33 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"react-use-size": "^3.0.3",
"react-windowed-select": "^5.2.0",
"reactflow": "^11.11.3",
"reaviz": "^16.1.2",
"recharts": "^2.15.4",
"shiki": "^3.20.0",
"streamdown": "^1.6.10",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const configChangeSeverity = {
icon: <HiInformationCircle className="text-gray-500" />,
name: "Info",
description: "Info",
value: "Info",
value: "info",
colorClass: "text-gray-500 fill-gray-500"
}
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ConfigChangeSeverity } from "./ConfigChangeSeverity";
import ConfigChangesDateRangeFilter from "./ConfigChangesDateRangeFIlter";
import { ConfigTagsDropdown } from "./ConfigTagsDropdown";
import ShowDeletedConfigs from "../../ConfigsListFilters/ShowDeletedConfigs";
import ConfigChangesViewToggle from "../ConfigChangesViewToggle";
import ConfigTypesTristateDropdown from "./ConfigTypesTristateDropdown";

type FilterBadgeProps = {
Expand Down Expand Up @@ -102,6 +103,8 @@ export function ConfigChangeFilters({
<ConfigTagsDropdown />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
<ShowDeletedConfigs />
<ConfigChangesViewToggle />

{extra}
</div>
</FormikFilterForm>
Expand Down
132 changes: 132 additions & 0 deletions src/components/Configs/Changes/ConfigChangesGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ConfigChange } from "@flanksource-ui/api/types/configs";
import { Age } from "@flanksource-ui/ui/Age";
import { ChangeIcon } from "@flanksource-ui/ui/Icons/ChangeIcon";
import { relativeDateTime } from "@flanksource-ui/utils/date";
import dayjs from "dayjs";
import { useMemo } from "react";
import {
ChartShallowDataShape,
ChartTooltip,
ChartZoomPan,
LinearXAxis,
LinearXAxisTickLabel,
LinearXAxisTickLine,
LinearXAxisTickSeries,
LinearYAxis,
LinearYAxisTickLabel,
LinearYAxisTickSeries,
ScatterPlot,
ScatterPoint,
ScatterSeries
} from "reaviz";
import ConfigsTypeIcon from "../ConfigsTypeIcon";

type ConfigChangesGraphProps = {
changes: ConfigChange[];
onItemClicked?: (change: ConfigChange) => void;
};

export default function ConfigChangesGraph({
changes,
onItemClicked = () => {}
}: ConfigChangesGraphProps) {
const data: ChartShallowDataShape[] = useMemo(() => {
return changes
.filter((change) => change.first_observed && change.config?.name)
.map((change) => ({
key: dayjs(change.first_observed).toDate(),
data: change.config!.name!,
metadata: change
}));
}, [changes]);

return (
<div className="h-full w-full">
<ScatterPlot
data={data}
zoomPan={<ChartZoomPan />}
yAxis={
<LinearYAxis
type="category"
tickSeries={
<LinearYAxisTickSeries
label={
<LinearYAxisTickLabel
padding={3}
fontSize={12}
format={(v) => v}
/>
}
/>
}
/>
}
xAxis={
<LinearXAxis
type="time"
scale={{ type: "time" }}
tickSeries={
<LinearXAxisTickSeries
line={<LinearXAxisTickLine position="center" />}
label={
<LinearXAxisTickLabel
padding={3}
format={(v) => relativeDateTime(v)}
/>
}
/>
}
/>
}
series={
<ScatterSeries
point={
<ScatterPoint
tooltip={
<ChartTooltip
followCursor={true}
content={(data: any) => {
const change = data.metadata as ConfigChange;
return (
<div className="flex flex-col gap-1 rounded-lg bg-gray-100 p-2 text-black shadow-sm">
<ConfigsTypeIcon config={change.config}>
{change.config?.name}
</ConfigsTypeIcon>
<div className="flex flex-col gap-1">
<div className="flex flex-row items-center justify-center gap-2 text-xs">
<span className="flex flex-row items-center gap-1 font-semibold">
<ChangeIcon change={change} />
{change.change_type}
</span>
<span className="font-semibold">
<Age from={change.created_at} />
{(change.count || 1) > 1 &&
change.first_observed && (
<span className="inline-block pl-1 text-gray-500">
(x{change.count} over{" "}
<Age from={change.first_observed} />)
</span>
)}
</span>
</div>
<p>{change.summary}</p>
</div>
</div>
);
}}
/>
}
className={"bg-gray-500"}
size={20}
symbol={(data) => <ChangeIcon change={data.metadata} />}
onClick={(data) => {
onItemClicked(data.metadata as ConfigChange);
}}
/>
}
/>
}
/>
</div>
);
}
34 changes: 34 additions & 0 deletions src/components/Configs/Changes/ConfigChangesViewToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Switch } from "@flanksource-ui/ui/FormControls/Switch";
import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";

export type GraphType = "Table" | "Graph";
export const configChangesViewToggle = atomWithStorage<GraphType>(
"configChangesViewToggleState",
"Table",
undefined,
{
getOnInit: true
}
);

export function useConfigChangesViewToggleState() {
const [view] = useAtom(configChangesViewToggle);
return view;
}

export default function ConfigChangesViewToggle() {
const [toggleValue, setToggleValue] = useAtom(configChangesViewToggle);

return (
<div className="ml-auto flex flex-row items-center gap-2 px-2">
<Switch
options={["Table", "Graph"]}
onChange={(v) => {
setToggleValue(v as GraphType);
}}
value={toggleValue}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function ConfigDetailsChanges({
<Stat
title="Date"
sizeStyle="sm"
value={<Age from={changeDetails?.created_at!} suffix={true} />}
value={<Age from={changeDetails?.created_at} suffix={true} />}
/>

<Stat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ConfigChangesDateRangeFilter from "../../ConfigChangesFilters/ConfigChang
import { FilterBadge } from "../../ConfigChangesFilters/ConfigChangesFilters";
import { ConfigRelatedChangesToggles } from "../../ConfigChangesFilters/ConfigRelatedChangesToggles";
import { ConfigTagsDropdown } from "../../ConfigChangesFilters/ConfigTagsDropdown";
import ConfigChangesViewToggle from "../../ConfigChangesViewToggle";
import ShowDeletedConfigs from "../../../ConfigsListFilters/ShowDeletedConfigs";
import ConfigTypesTristateDropdown from "../../ConfigChangesFilters/ConfigTypesTristateDropdown";

Expand All @@ -29,14 +30,21 @@ export function ConfigRelatedChangesFilters({
paramsToReset={paramsToReset}
filterFields={["configTypes", "changeType", "severity", "tags"]}
>
<div className={clsx("flex flex-wrap items-center gap-2", className)}>
<div
className={clsx(
"flex w-full flex-wrap items-center gap-2",
className
)}
>
<ConfigTypesTristateDropdown />
<ChangesTypesDropdown />
<ConfigChangeSeverity />
<ConfigTagsDropdown />
<ConfigRelatedChangesToggles />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
<ShowDeletedConfigs />
<ConfigChangesViewToggle />

{extra}
</div>
</FormikFilterForm>
Expand Down
62 changes: 54 additions & 8 deletions src/pages/config/ConfigChangesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useGetAllConfigsChangesQuery } from "@flanksource-ui/api/query-hooks/useConfigChangesHooks";
import { useGetConfigChangesById } from "@flanksource-ui/api/query-hooks/useGetConfigChangesByConfigChangeIdQuery";
import { ConfigChange } from "@flanksource-ui/api/types/configs";
import { ConfigChangeTable } from "@flanksource-ui/components/Configs/Changes/ConfigChangeTable";
import { ConfigChangeFilters } from "@flanksource-ui/components/Configs/Changes/ConfigChangesFilters/ConfigChangesFilters";
import { useConfigChangesViewToggleState } from "@flanksource-ui/components/Configs/Changes/ConfigChangesViewToggle";
import { ConfigDetailChangeModal } from "@flanksource-ui/components/Configs/Changes/ConfigDetailsChanges/ConfigDetailsChanges";
import ConfigPageTabs from "@flanksource-ui/components/Configs/ConfigPageTabs";
import ConfigsTypeIcon from "@flanksource-ui/components/Configs/ConfigsTypeIcon";
import { InfoMessage } from "@flanksource-ui/components/InfoMessage";
Expand All @@ -15,7 +18,7 @@ import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout";
import { refreshButtonClickedTrigger } from "@flanksource-ui/ui/SlidingSideBar/SlidingSideBar";
import { Toggle } from "@flanksource-ui/ui/FormControls/Toggle";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { Suspense, lazy, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";

function getNewestInsertedAt(changes: ConfigChange[]): string | undefined {
Expand All @@ -29,7 +32,12 @@ function getNewestInsertedAt(changes: ConfigChange[]): string | undefined {
return latest;
}

const ConfigChangesGraph = lazy(
() => import("@flanksource-ui/components/Configs/Changes/ConfigChangesGraph")
);

export function ConfigChangesPage() {
const view = useConfigChangesViewToggleState();
const [, setRefreshButtonClickedTrigger] = useAtom(
refreshButtonClickedTrigger
);
Expand Down Expand Up @@ -125,6 +133,17 @@ export function ConfigChangesPage() {
const totalChanges = (data?.total ?? 0) + newTailedCount;
const totalChangesPages = Math.ceil(totalChanges / parseInt(pageSize));

const [selectedChange, setSelectedChange] = useState<ConfigChange>();

useEffect(() => {
if (view !== "Graph") setSelectedChange(undefined);
}, [view]);

const { data: changeDetails, isLoading: changeLoading } =
useGetConfigChangesById(selectedChange?.id ?? "", {
enabled: view === "Graph" && !!selectedChange
});

const errorMessage =
typeof error === "string"
? error
Expand Down Expand Up @@ -185,13 +204,40 @@ export function ConfigChangesPage() {
/>
}
/>
<ConfigChangeTable
data={changes}
isLoading={isLoading}
isRefetching={isRefetching}
totalRecords={totalChanges}
numberOfPages={totalChangesPages}
/>
{view === "Graph" ? (
<>
<Suspense
fallback={
<div className="flex h-full items-center justify-center">
Loading graph...
</div>
}
>
<ConfigChangesGraph
changes={changes}
onItemClicked={(change) => setSelectedChange(change)}
/>
</Suspense>
{selectedChange && (
<ConfigDetailChangeModal
isLoading={changeLoading}
open={!!selectedChange}
setOpen={(open) => {
if (!open) setSelectedChange(undefined);
}}
changeDetails={changeDetails ?? selectedChange}
/>
)}
</>
) : (
<ConfigChangeTable
data={changes}
isLoading={isLoading}
isRefetching={isRefetching}
totalRecords={totalChanges}
numberOfPages={totalChangesPages}
/>
)}
</>
)}
</ConfigPageTabs>
Expand Down
Loading
Loading