Skip to content
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
167 changes: 167 additions & 0 deletions pkg/app/web/src/components/application-list-item.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import userEvent from "@testing-library/user-event";
import React from "react";
import { MemoryRouter } from "react-router-dom";
import { createStore, render, screen } from "../../test-utils";
import { dummyApplication } from "../__fixtures__/dummy-application";
import { ApplicationListItem } from "./application-list-item";

const state = {
applications: {
entities: {
[dummyApplication.id]: dummyApplication,
},
ids: [dummyApplication.id],
},
};

test("delete", () => {
const handleDelete = jest.fn();
const store = createStore(state);
render(
<MemoryRouter>
<table>
<tbody>
<ApplicationListItem
applicationId={dummyApplication.id}
onEdit={() => null}
onEnable={() => null}
onDisable={() => null}
onDelete={handleDelete}
onEncryptSecret={() => null}
/>
</tbody>
</table>
</MemoryRouter>,
{
store,
}
);

userEvent.click(screen.getByRole("button", { name: "Open menu" }));
userEvent.click(screen.getByRole("menuitem", { name: "Delete" }));

expect(handleDelete).toHaveBeenCalledWith(dummyApplication.id);
});

test("edit", () => {
const handleEdit = jest.fn();
const store = createStore(state);
render(
<MemoryRouter>
<table>
<tbody>
<ApplicationListItem
applicationId={dummyApplication.id}
onEdit={handleEdit}
onEnable={() => null}
onDisable={() => null}
onDelete={() => null}
onEncryptSecret={() => null}
/>
</tbody>
</table>
</MemoryRouter>,
{
store,
}
);

userEvent.click(screen.getByRole("button", { name: "Open menu" }));
userEvent.click(screen.getByRole("menuitem", { name: "Edit" }));

expect(handleEdit).toHaveBeenCalledWith(dummyApplication.id);
});

test("disable", () => {
const handleDisable = jest.fn();
const store = createStore(state);
render(
<MemoryRouter>
<table>
<tbody>
<ApplicationListItem
applicationId={dummyApplication.id}
onEdit={() => null}
onEnable={() => null}
onDisable={handleDisable}
onDelete={() => null}
onEncryptSecret={() => null}
/>
</tbody>
</table>
</MemoryRouter>,
{
store,
}
);

userEvent.click(screen.getByRole("button", { name: "Open menu" }));
userEvent.click(screen.getByRole("menuitem", { name: "Disable" }));

expect(handleDisable).toHaveBeenCalledWith(dummyApplication.id);
});

test("enable", () => {
const handleEnable = jest.fn();
const store = createStore({
applications: {
entities: {
[dummyApplication.id]: { ...dummyApplication, disabled: true },
},
ids: [dummyApplication.id],
},
});
render(
<MemoryRouter>
<table>
<tbody>
<ApplicationListItem
applicationId={dummyApplication.id}
onEdit={() => null}
onEnable={handleEnable}
onDisable={() => null}
onDelete={() => null}
onEncryptSecret={() => null}
/>
</tbody>
</table>
</MemoryRouter>,
{
store,
}
);

userEvent.click(screen.getByRole("button", { name: "Open menu" }));
userEvent.click(screen.getByRole("menuitem", { name: "Enable" }));

expect(handleEnable).toHaveBeenCalledWith(dummyApplication.id);
});

test("Encrypt Secret", () => {
const handleGenerateSecret = jest.fn();
const store = createStore(state);
render(
<MemoryRouter>
<table>
<tbody>
<ApplicationListItem
applicationId={dummyApplication.id}
onEdit={() => null}
onEnable={() => null}
onDisable={() => null}
onDelete={() => null}
onEncryptSecret={handleGenerateSecret}
/>
</tbody>
</table>
</MemoryRouter>,
{
store,
}
);

userEvent.click(screen.getByRole("button", { name: "Open menu" }));
userEvent.click(screen.getByRole("menuitem", { name: "Encrypt Secret" }));

expect(handleGenerateSecret).toHaveBeenCalledWith(dummyApplication.id);
});
194 changes: 194 additions & 0 deletions pkg/app/web/src/components/application-list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
Box,
IconButton,
Link,
makeStyles,
Menu,
MenuItem,
TableCell,
TableRow,
Typography,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/MoreVert";
import dayjs from "dayjs";
import React, { FC, memo, useState } from "react";
import { useSelector } from "react-redux";
import { Link as RouterLink } from "react-router-dom";
import { APPLICATION_KIND_TEXT } from "../constants/application-kind";
import { APPLICATION_SYNC_STATUS_TEXT } from "../constants/application-sync-status-text";
import { PAGE_PATH_APPLICATIONS } from "../constants/path";
import { AppState } from "../modules";
import { Application, selectById } from "../modules/applications";
import {
Environment,
selectById as selectEnvById,
} from "../modules/environments";
import { SyncStatusIcon } from "./sync-status-icon";
import clsx from "clsx";

const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
flex: 1,
overflow: "auto",
},
statusText: {
marginLeft: theme.spacing(1),
},
disabled: {
background: theme.palette.grey[200],
},
}));

const EmptyDeploymentData: FC = () => (
<>
<TableCell>{NOT_AVAILABLE_TEXT}</TableCell>
<TableCell>{NOT_AVAILABLE_TEXT}</TableCell>
<TableCell>{NOT_AVAILABLE_TEXT}</TableCell>
<TableCell>{NOT_AVAILABLE_TEXT}</TableCell>
</>
);

const NOT_AVAILABLE_TEXT = "N/A";

interface Props {
applicationId: string;
onEdit: (id: string) => void;
onEnable: (id: string) => void;
onDisable: (id: string) => void;
onDelete: (id: string) => void;
onEncryptSecret: (id: string) => void;
}

export const ApplicationListItem: FC<Props> = memo(
function ApplicationListItem({
applicationId,
onDisable,
onEdit,
onEnable,
onDelete,
onEncryptSecret,
}) {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const app = useSelector<AppState, Application | undefined>((state) =>
selectById(state.applications, applicationId)
);
const env = useSelector<AppState, Environment | undefined>((state) =>
app ? selectEnvById(state.environments, app.envId) : undefined
);

const handleEdit = (): void => {
setAnchorEl(null);
onEdit(applicationId);
};

const handleDisable = (): void => {
setAnchorEl(null);
onDisable(applicationId);
};

const handleEnable = (): void => {
setAnchorEl(null);
onEnable(applicationId);
};

const handleDelete = (): void => {
setAnchorEl(null);
onDelete(applicationId);
};

const handleGenerateSecret = (): void => {
setAnchorEl(null);
onEncryptSecret(applicationId);
};

if (!app) {
return null;
}

const recentlyDeployment = app.mostRecentlySuccessfulDeployment;

return (
<>
<TableRow className={clsx({ [classes.disabled]: app.disabled })}>
<TableCell>
<Box display="flex" alignItems="center">
{app.syncState ? (
<>
<SyncStatusIcon status={app.syncState.status} />
<Typography className={classes.statusText}>
{APPLICATION_SYNC_STATUS_TEXT[app.syncState.status]}
</Typography>
</>
) : (
NOT_AVAILABLE_TEXT
)}
</Box>
</TableCell>
<TableCell>
<Link
component={RouterLink}
to={`${PAGE_PATH_APPLICATIONS}/${app.id}`}
>
{app.name}
</Link>
</TableCell>
<TableCell>{APPLICATION_KIND_TEXT[app.kind]}</TableCell>
<TableCell>{env?.name}</TableCell>
{recentlyDeployment ? (
<>
<TableCell>{recentlyDeployment.version}</TableCell>
<TableCell>
{recentlyDeployment.trigger?.commit?.hash.slice(0, 8) ??
NOT_AVAILABLE_TEXT}
</TableCell>
<TableCell>
{recentlyDeployment.trigger?.commander ||
recentlyDeployment.trigger?.commit?.author ||
NOT_AVAILABLE_TEXT}
</TableCell>
<TableCell>
{dayjs(recentlyDeployment.startedAt * 1000).fromNow()}
</TableCell>
</>
) : (
<EmptyDeploymentData />
)}
<TableCell align="right">
<IconButton
aria-label="Open menu"
onClick={(e) => {
setAnchorEl(e.currentTarget);
}}
>
<MenuIcon />
</IconButton>
</TableCell>
</TableRow>

<Menu
id="application-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
PaperProps={{
style: {
width: "20ch",
},
}}
>
<MenuItem onClick={handleEdit}>Edit</MenuItem>
<MenuItem onClick={handleGenerateSecret}>Encrypt Secret</MenuItem>
{app && app.disabled ? (
<MenuItem onClick={handleEnable}>Enable</MenuItem>
) : (
<MenuItem onClick={handleDisable}>Disable</MenuItem>
)}
<MenuItem onClick={handleDelete}>Delete</MenuItem>
</Menu>
</>
);
}
);
Loading