Skip to content

Feat/insight UI update #5633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 7, 2025
Merged
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
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,6 @@
"react-syntax-highlighter": "^15.6.1",
"redux-thunk": "^2.4.1",
"yup": "^0.32.11"
}
},
"packageManager": "[email protected]+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
48 changes: 48 additions & 0 deletions web/src/components/chart-empty-data/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box, makeStyles, Typography } from "@material-ui/core";
import { WarningOutlined } from "@material-ui/icons";
import { FC } from "react";

type Props = {
visible: boolean;
noDataText?: string;
};

const useStyles = makeStyles((theme) => ({
noDataMessage: {
display: "flex",
},
noDataMessageIcon: {
marginRight: theme.spacing(1),
},
}));

const NO_DATA_TEXT = "No data is available.";

const ChartEmptyData: FC<Props> = ({ visible, noDataText = NO_DATA_TEXT }) => {
const classes = useStyles();

return (
<Box
display={visible ? "flex" : "none"}
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
position="absolute"
top={0}
left={0}
bgcolor="#fafafabb"
>
<Typography
variant="body1"
color="textSecondary"
className={classes.noDataMessage}
>
<WarningOutlined className={classes.noDataMessageIcon} />
{noDataText}
</Typography>
</Box>
);
};

export default ChartEmptyData;
42 changes: 14 additions & 28 deletions web/src/components/insight-page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { Box } from "@material-ui/core";
import { FC, memo, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { FC, memo, useEffect } from "react";
import { useAppDispatch, useAppSelector } from "~/hooks/redux";
import { PAGE_PATH_APPLICATIONS } from "~/constants/path";
import {
ApplicationKind,
fetchApplications,
selectById,
} from "~/modules/applications";
import { fetchApplications, selectById } from "~/modules/applications";
import { fetchApplicationCount } from "~/modules/application-counts";
import {
InsightDataPoint,
InsightResolution,
InsightRange,
} from "~/modules/insight";
import { ApplicationCounts } from "./application-counts";
import { ChangeFailureRateChart } from "./change-failure-rate-chart";
import { DeploymentFrequencyChart } from "./deployment-frequency-chart";
import { InsightHeader } from "./insight-header";
import { fetchDeploymentChangeFailureRate } from "~/modules/deployment-change-failure-rate";
import { fetchDeploymentFrequency } from "~/modules/deployment-frequency";
import {
fetchDeploymentChangeFailureRate,
fetchDeploymentChangeFailureRate24h,
} from "~/modules/deployment-change-failure-rate";
import {
fetchDeployment24h,
fetchDeploymentFrequency,
} from "~/modules/deployment-frequency";
import { StatisticInformation } from "./statistic-information";

export const InsightIndexPage: FC = memo(function InsightIndexPage() {
const dispatch = useAppDispatch();
const navigate = useNavigate();

const [applicationId, labels, range, resolution] = useAppSelector<
[string, Array<string>, InsightRange, InsightResolution]
Expand Down Expand Up @@ -81,28 +80,15 @@ export const InsightIndexPage: FC = memo(function InsightIndexPage() {

useEffect(() => {
dispatch(fetchDeploymentFrequency());
dispatch(fetchDeployment24h());
dispatch(fetchDeploymentChangeFailureRate());
console.log("deployment insights should be loaded");
dispatch(fetchDeploymentChangeFailureRate24h());
}, [dispatch, applicationId, labels, range, resolution]);

const updateURL = useCallback(
(kind: ApplicationKind) => {
navigate(`${PAGE_PATH_APPLICATIONS}?kind=${kind}`, { replace: true });
},
[navigate]
);

const handleApplicationCountClick = useCallback(
(kind: ApplicationKind) => {
updateURL(kind);
},
[updateURL]
);

return (
<Box flex={1} p={2} overflow="auto">
<Box display="flex" flexDirection="column" flex={1} p={2}>
<ApplicationCounts onClick={handleApplicationCountClick} />
<StatisticInformation />
</Box>
<InsightHeader />
<Box
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
Box,
Card,
CardContent,
makeStyles,
Typography,
} from "@material-ui/core";
import clsx from "clsx";
import { FC, useEffect, useMemo } from "react";
import {
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from "echarts/components";
import * as echarts from "echarts/core";
import { BarChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import grey from "@material-ui/core/colors/grey";
import lineColor from "@material-ui/core/colors/purple";
import { useAppSelector } from "~/hooks/redux";
import { selectAll as selectAllApplications } from "~/modules/applications";
import { selectAllPipeds } from "~/modules/pipeds";
import ChartEmptyData from "~/components/chart-empty-data";
import useEChartState from "~/hooks/useEChartState";

const useStyles = makeStyles(() => ({
root: {
minWidth: 300,
display: "inline-block",
overflow: "visible",
position: "relative",
},
pageTitle: {
fontWeight: "bold",
},
}));

const ApplicationByPiped: FC = () => {
const classes = useStyles();
const { chart, chartElm } = useEChartState({
extensions: [
TitleComponent,
TooltipComponent,
GridComponent,
BarChart,
CanvasRenderer,
LegendComponent,
],
});
const applications = useAppSelector((state) =>
selectAllApplications(state.applications)
);

const pipedList = useAppSelector(selectAllPipeds);

const data: { name: string; count: number; rank: number }[] = useMemo(() => {
const pipedMap = pipedList.reduce((acc, piped) => {
acc[piped.id] = { name: piped.name, id: piped.id, count: 0 };
return acc;
}, {} as Record<string, { name: string; id: string; count: number }>);

applications.forEach((app) => {
if (!app.pipedId) return;

if (app.pipedId in pipedMap === false) {
return;
}

if (app.pipedId in pipedMap) {
pipedMap[app.pipedId].count += 1;
}
});

const listAppsByPipedSorted = Object.values(pipedMap)
.filter((v) => v.count > 0)
.sort((a, b) => b.count - a.count);

const itemRank1st = listAppsByPipedSorted[0];
const itemRank2nd = listAppsByPipedSorted[1];
const itemRank3rd = listAppsByPipedSorted[2];

const list = [];

if (itemRank2nd) list.push({ ...itemRank2nd, rank: 2 });
if (itemRank1st) list.push({ ...itemRank1st, rank: 1 });
if (itemRank3rd) list.push({ ...itemRank3rd, rank: 3 });

return list;
}, [applications, pipedList]);

const yMax = useMemo(() => {
return Math.max(...data.map((v) => v.count));
}, [data]);

const isNoData = data.length === 0;

useEffect(() => {
if (chart && data.length !== 0) {
chart.setOption({
grid: {
top: 50,
bottom: 0,
left: 0,
right: 0,
},
xAxis: {
data: data.map((v) => v.name),
axisLine: { show: false },
axisLabel: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
yAxis: {
axisLine: { show: false },
axisLabel: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
tooltip: { show: true },
series: [
{
name: "Applications",
type: "bar",
stack: "title",
data: data.map((v) => ({
value: v.count,
label: {
show: true,
formatter: v.rank === 1 ? "{b}\n{c} apps" : "{c} apps",
align: "center",
verticalAlign: "bottom",
position: "top",
distance: 10,
width: 200,
overflow: "truncate",
},
itemStyle: {
color: v.rank === 1 ? lineColor[500] : grey[300],
borderRadius: 5,
},
})),
},
],
} as echarts.EChartsInitOpts);
}
}, [chart, data, isNoData, yMax]);

return (
<Card raised className={clsx(classes.root)}>
<CardContent>
<Typography
color="textSecondary"
gutterBottom
className={classes.pageTitle}
>
Application by piped
</Typography>

<Box position={"relative"}>
<div style={{ width: "100%", height: 200 }} ref={chartElm} />
<ChartEmptyData visible={!data.length} />
</Box>
</CardContent>
</Card>
);
};

export default ApplicationByPiped;
Loading