Skip to content

Commit

Permalink
Draft UI for deployment traces feature (#5594)
Browse files Browse the repository at this point in the history
Signed-off-by: kypham <[email protected]>
  • Loading branch information
hongky-1994 authored Feb 26, 2025
1 parent 330c622 commit 42df575
Show file tree
Hide file tree
Showing 18 changed files with 1,119 additions and 2 deletions.
21 changes: 21 additions & 0 deletions web/src/__fixtures__/dummy-deployment-trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { dummyDeployment } from "./dummy-deployment";
import { createRandTimes, randomUUID } from "./utils";
import { ListDeploymentTracesResponse } from "~~/api_client/service_pb";

const [createdAt, completedAt] = createRandTimes(3);

export const dummyDeploymentTrace: ListDeploymentTracesResponse.DeploymentTraceRes.AsObject = {
trace: {
id: randomUUID(),
title: "title",
author: "user",
commitTimestamp: createdAt.unix(),
commitMessage: "commit-message",
commitHash: "commit-hash",
commitUrl: "commit-url",
createdAt: createdAt.unix(),
updatedAt: completedAt.unix(),
completedAt: completedAt.unix(),
},
deploymentsList: [dummyDeployment],
};
25 changes: 25 additions & 0 deletions web/src/api/deploymentTraces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
ListDeploymentTracesRequest,
ListDeploymentTracesResponse,
} from "~~/api_client/service_pb";
import { apiClient, apiRequest } from "./client";

export const getDeploymentTraces = ({
options,
pageSize,
cursor,
pageMinUpdatedAt,
}: ListDeploymentTracesRequest.AsObject): Promise<
ListDeploymentTracesResponse.AsObject
> => {
const req = new ListDeploymentTracesRequest();
if (options) {
const opts = new ListDeploymentTracesRequest.Options();
opts.setCommitHash(options.commitHash);
req.setOptions(opts);
req.setPageSize(pageSize);
req.setCursor(cursor);
req.setPageMinUpdatedAt(pageMinUpdatedAt);
}
return apiRequest(req, apiClient.listDeploymentTraces);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen, fireEvent } from "@testing-library/react";
import DeploymentTraceFilter from "./index";
import { MemoryRouter } from "~~/test-utils";

jest.useFakeTimers();

describe("DeploymentTraceFilter", () => {
const mockOnChange = jest.fn();
const mockOnClear = jest.fn();
const filterValues = { commitHash: "12345" };

beforeEach(() => {
render(
<MemoryRouter>
<DeploymentTraceFilter
filterValues={filterValues}
onChange={mockOnChange}
onClear={mockOnClear}
/>
</MemoryRouter>
);
});

it("should render filter inputs", () => {
expect(
screen.getByRole("textbox", { name: /commit hash/i })
).toBeInTheDocument();
});

it("should call onChange when filter value changes", () => {
const input = screen.getByRole("textbox", { name: /commit hash/i });
fireEvent.change(input, { target: { value: "67890" } });

jest.runAllTimers();
expect(mockOnChange).toHaveBeenCalledWith({ commitHash: "67890" });
});

it("should call onClear when clear button is clicked", () => {
const clearButton = screen.getByRole("button", { name: /clear/i });
fireEvent.click(clearButton);
expect(mockOnClear).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { makeStyles, TextField } from "@material-ui/core";
import { FC, useMemo, useState } from "react";
import { FilterView } from "~/components/filter-view";
import debounce from "~/utils/debounce";

type Props = {
filterValues: { commitHash?: string };
onClear: () => void;
onChange: (options: { commitHash?: string }) => void;
};

const useStyles = makeStyles((theme) => ({
formItem: {
width: "100%",
marginTop: theme.spacing(4),
},
}));

const DEBOUNCE_INPUT_WAIT = 1000;

const DeploymentTraceFilter: FC<Props> = ({
filterValues,
onClear,
onChange,
}) => {
const classes = useStyles();
const [commitHash, setCommitHash] = useState<string | null>(
filterValues.commitHash ?? ""
);

const debounceChangeCommitHash = useMemo(
() => debounce(onChange, DEBOUNCE_INPUT_WAIT),
[onChange]
);

const onChangeCommitHash = (commitHash: string): void => {
debounceChangeCommitHash({ commitHash: commitHash });
};

return (
<FilterView
onClear={() => {
onClear();
setCommitHash("");
}}
>
<div className={classes.formItem}>
<TextField
id="commit-hash"
label="Commit hash"
variant="outlined"
fullWidth
value={commitHash || ""}
onChange={(e) => {
const text = e.target.value;
setCommitHash(text);
onChangeCommitHash(text);
}}
/>
</div>
</FilterView>
);
};

export default DeploymentTraceFilter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import DeploymentItem from "./deployment-item";
import { dummyDeployment } from "~/__fixtures__/dummy-deployment";
import { render } from "~~/test-utils";

describe("DeploymentItem", () => {
it("should render deployment item with correct data", () => {
const { getByText } = render(
<DeploymentItem deployment={dummyDeployment} />,
{}
);
const expectedValues = {
status: "SUCCESS",
applicationName: "DemoApp",
kind: "KUBERNETES",
description:
"Quick sync by deploying the new version and configuring all traffic to it because no pipeline was configured",
};

Object.values(expectedValues).forEach((value) => {
expect(getByText(value)).toBeInTheDocument();
});
});

it("should display 'No description.' when summary is empty", () => {
const deploymentWithoutSummary = { ...dummyDeployment, summary: "" };
const { getByText } = render(
<DeploymentItem deployment={deploymentWithoutSummary} />,
{}
);

expect(getByText("No description.")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Box, Chip, makeStyles, Typography } from "@material-ui/core";
import dayjs from "dayjs";
import { FC } from "react";
import { DeploymentStatusIcon } from "~/components/deployment-status-icon";
import { APPLICATION_KIND_TEXT } from "~/constants/application-kind";
import { DEPLOYMENT_STATE_TEXT } from "~/constants/deployment-status-text";
import { ellipsis } from "~/styles/text";
import { Deployment } from "~~/model/deployment_pb";

type Props = {
deployment: Deployment.AsObject;
};

const useStyles = makeStyles((theme) => ({
root: {
flex: 1,
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
// backgroundColor: theme.palette.background.paper,
},
info: {
marginLeft: theme.spacing(1),
},
statusText: {
marginLeft: theme.spacing(1),
lineHeight: "1.5rem",
// Fix width to prevent misalignment of application name.
width: "100px",
},
description: {
...ellipsis,
color: theme.palette.text.hint,
},
labelChip: {
marginLeft: theme.spacing(1),
marginBottom: theme.spacing(0.25),
},
}));

enum PipedVersion {
V0 = "v0",
V1 = "v1",
}

const NO_DESCRIPTION = "No description.";

const DeploymentItem: FC<Props> = ({ deployment }) => {
const classes = useStyles();

const pipedVersion =
!deployment.platformProvider ||
deployment?.deployTargetsByPluginMap?.length > 0
? PipedVersion.V1
: PipedVersion.V0;

return (
<Box className={classes.root}>
<Box display="flex" alignItems="center">
<DeploymentStatusIcon status={deployment.status} />
<Typography
variant="subtitle2"
className={classes.statusText}
component="span"
>
{DEPLOYMENT_STATE_TEXT[deployment.status]}
</Typography>
</Box>
<Box
display="flex"
flexDirection="column"
flex={1}
pl={2}
overflow="hidden"
>
<Box display="flex" alignItems="baseline">
<Typography variant="h6" component="span">
{deployment.applicationName}
</Typography>
<Typography
variant="body2"
color="textSecondary"
className={classes.info}
>
{pipedVersion === PipedVersion.V0 &&
APPLICATION_KIND_TEXT[deployment.kind]}
{pipedVersion === PipedVersion.V1 && "APPLICATION"}
{deployment?.labelsMap.map(([key, value], i) => (
<Chip
label={key + ": " + value}
className={classes.labelChip}
key={i}
/>
))}
</Typography>
</Box>
<Typography variant="body1" className={classes.description}>
{deployment.summary || NO_DESCRIPTION}
</Typography>
</Box>
<div>{dayjs(deployment.createdAt * 1000).fromNow()}</div>
</Box>
);
};

export default DeploymentItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { fireEvent, render, screen } from "@testing-library/react";
import DeploymentTraceItem from "./index";
import { dummyDeploymentTrace } from "~/__fixtures__/dummy-deployment-trace";
import { MemoryRouter } from "~~/test-utils";

describe("DeploymentTraceItem", () => {
it("should render trace information", () => {
render(
<MemoryRouter>
<DeploymentTraceItem
trace={dummyDeploymentTrace.trace}
deploymentList={dummyDeploymentTrace.deploymentsList}
/>
</MemoryRouter>
);

const expectedValues = {
title: "title",
author: "user",
commitMessage: "commit-message",
commitHash: "commit-hash",
commitUrl: "/commit-url",
};

expect(screen.getByText(expectedValues.title)).toBeInTheDocument();
expect(
screen.getByText(expectedValues.author + " authored")
).toBeInTheDocument();
expect(screen.getByText(expectedValues.commitMessage)).toBeInTheDocument();
expect(screen.getByText(expectedValues.commitHash)).toBeInTheDocument();
expect(screen.getByRole("link")).toHaveAttribute(
"href",
expectedValues.commitUrl
);
});

it("should render deployment items", () => {
render(
<MemoryRouter>
<DeploymentTraceItem
trace={dummyDeploymentTrace.trace}
deploymentList={dummyDeploymentTrace.deploymentsList}
/>
</MemoryRouter>
);
const buttonExpand = screen.getByRole("button", { name: /expand/i });
fireEvent.click(buttonExpand);
expect(screen.getByText("DemoApp")).toBeInTheDocument();
});
});
Loading

0 comments on commit 42df575

Please sign in to comment.