Skip to content
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

544 better ux for apps list #547

Merged
merged 5 commits into from
Apr 5, 2024
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
1 change: 1 addition & 0 deletions src/layouts/AppLayout/AppLayout.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ describe("~/layouts/AppLayout/Applayout.tsx", () => {
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/snippets`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/feature-flags`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/function-triggers`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/key-value`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/analytics`,
]);
});
Expand Down
1 change: 1 addition & 0 deletions src/layouts/AppLayout/EnvMenu.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe("~/layouts/AppLayout/EnvMenu.tsx", () => {
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/snippets`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/feature-flags`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/function-triggers`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/key-value`,
`/apps/${defaultApp.id}/environments/${defaultEnvs[0].id}/analytics`,
]);
});
Expand Down
168 changes: 73 additions & 95 deletions src/pages/apps/Apps.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { LocalStorage } from "~/utils/storage";
import { LS_PROVIDER } from "~/utils/api/Api";
import Card from "~/components/Card";
import CardHeader from "~/components/CardHeader";
import CardFooter from "~/components/CardFooter";
import CardRow from "~/components/CardRow";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import LoadingButton from "@mui/lab/LoadingButton";
import ImportExport from "@mui/icons-material/ImportExport";
import Link from "@mui/material/Link";
import LinkIcon from "@mui/icons-material/Link";
import SearchIcon from "@mui/icons-material/Search";
import ArrowForward from "@mui/icons-material/ArrowForwardIos";
import { AuthContext } from "~/pages/auth/Auth.context";
import ButtonDropdown from "~/components/ButtonDropdown";
import AppName from "~/components/AppName";
import InfoBox from "~/components/InfoBoxV2";
import Spinner from "~/components/Spinner";
import Dot from "~/components/Dot";
import { parseRepo, getLogoForProvider } from "~/utils/helpers/providers";
import { useSelectedTeam } from "~/layouts/TopMenu/Teams/actions";
import { providerToText } from "~/utils/helpers/string";
import { useFetchAppList } from "./actions";
import { WelcomeModal, EmptyList } from "./_components";
import { grey } from "@mui/material/colors";

let timeout: NodeJS.Timeout;
const limit = 20;
const welcomeModalId = "welcome_modal";

export default function Apps() {
const navigate = useNavigate();
const { teams } = useContext(AuthContext);
const [from, setFrom] = useState(0);
const [filter, setFilter] = useState("");
Expand Down Expand Up @@ -56,27 +59,6 @@ export default function Apps() {
}

const importFromProvider = `Import from ${providerToText[provider]}`;
const isLoadingFirstTime = loading && apps.length === 0;

if (isLoadingFirstTime) {
return (
<Box maxWidth="md" sx={{ width: "100%", color: "white" }}>
<Card>
<Box
sx={{
p: 4,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "200px",
}}
>
<Spinner primary width={8} height={8} />
</Box>
</Card>
</Box>
);
}

if (apps.length === 0 && !loading && !filter) {
return (
Expand All @@ -91,11 +73,12 @@ export default function Apps() {
return (
<Box maxWidth="md" sx={{ width: "100%", color: "white" }}>
<Card
contentPadding={false}
errorTitle={false}
loading={loading}
error={
!loading && apps.length === 0
? "This search produced no results."
: ""
error ||
(!loading && apps.length === 0 && "This search produced no results.")
}
>
<CardHeader
Expand All @@ -118,15 +101,14 @@ export default function Apps() {
/>
}
/>
<Box>
<Box sx={{ mx: 4, mb: 2 }}>
<TextField
fullWidth
autoFocus
placeholder="Search"
aria-label="Search apps"
label="Search apps"
variant="filled"
sx={{ mb: 4 }}
onChange={e => {
clearTimeout(timeout);
timeout = setTimeout(() => {
Expand All @@ -138,76 +120,72 @@ export default function Apps() {
endAdornment: <SearchIcon sx={{ fontSize: 16 }} />,
}}
/>
{!loading && error && <InfoBox type={InfoBox.ERROR}>{error}</InfoBox>}
{!error && (
<>
<Box>
{apps.map(app => (
<Box
key={app.id}
bgcolor="container.paper"
sx={{
p: 2,
mb: 2,
width: "100%",
cursor: "pointer",
"&:hover": {
filter: "brightness(1.5)",
transition: "all 0.25s ease-in",
},
":last-child": {
mb: 0,
},
}}
tabIndex={0}
role="button"
onKeyUp={e => {
if (e.key === "Enter") {
navigate(`/apps/${app.id}/environments`);
}
}}
onClick={() => {
navigate(`/apps/${app.id}/environments`);
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ display: "flex", flex: 1 }}>
<AppName
displayName={app.displayName}
repo={app.repo}
imageSx={{ width: 23 }}
/>
</Box>
<ArrowForward sx={{ fontSize: 16 }} />
</Box>
</Box>
))}
</Box>
{hasNextPage && (
<div className="my-4 flex justify-center">
<LoadingButton
variant="contained"
color="secondary"
loading={loading}
onClick={() => {
setFrom(from + limit);
}}
>
Load more
</LoadingButton>
</div>
)}
</>
)}
</Box>
</Card>
{!isLoadingFirstTime && (
{apps.map(app => {
const { repo, provider } = parseRepo(app.repo);
const providerLogo = getLogoForProvider(provider);
const environmentsUrl = `/apps/${app.id}/environments`;

return (
<CardRow
key={app.id}
actions={
<IconButton href={environmentsUrl}>
<ArrowForward sx={{ fontSize: 16 }} />
</IconButton>
}
>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
}}
>
<Box
component="img"
sx={{
display: "inline-block",
mr: 1,
width: 20,
}}
src={providerLogo}
alt={provider}
/>

<Typography>
<Link href={environmentsUrl}>{app.displayName}</Link>
</Typography>
<Dot />
<Typography color={grey[500]}>{repo}</Typography>
</Box>
</CardRow>
);
})}
{hasNextPage && (
<CardFooter
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<LoadingButton
variant="text"
loading={loading}
onClick={() => {
setFrom(from + limit);
}}
>
Load more
</LoadingButton>
</CardFooter>
)}
<WelcomeModal
isOpen={isWelcomeModalOpen}
toggleModal={setIsWelcomeModalOpen}
welcomeModalId={welcomeModalId}
/>
)}
</Card>
</Box>
);
}
2 changes: 1 addition & 1 deletion src/shared/deployments/AppChip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("~/shared/deployments/AppChip.tsx", () => {
const createWrapper = ({ deployment }: Props) => {
wrapper = render(
<MemoryRouter>
<AppChip deployment={deployment} />
<AppChip repo={deployment.repo} envName={deployment.envName} />
</MemoryRouter>
);
};
Expand Down
18 changes: 12 additions & 6 deletions src/shared/deployments/AppChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Dot from "~/components/Dot";
import githubLogo from "~/assets/logos/github-logo.svg";
import bitbucketLogo from "~/assets/logos/bitbucket-logo.svg";
import gitlabLogo from "~/assets/logos/gitlab-logo.svg";
import { parseRepo } from "~/utils/helpers/providers";

const logos: Record<string, string> = {
github: githubLogo,
Expand All @@ -14,16 +15,21 @@ const logos: Record<string, string> = {
};

interface Props {
repo: string;
sx?: SxProps;
children?: React.ReactNode;
deployment: DeploymentV2;
envName?: string;
color?: "secondary" | "info";
}

export default function AppChip({ children, deployment, color, sx }: Props) {
const pieces = deployment.repo.split("/");
const provider = pieces.shift();
const repo = pieces.join("/");
export default function AppChip({
children,
repo: repoNameWithProvider,
envName,
color,
sx,
}: Props) {
const { repo, provider } = parseRepo(repoNameWithProvider);

return (
<Chip
Expand All @@ -36,7 +42,7 @@ export default function AppChip({ children, deployment, color, sx }: Props) {
<Dot />
</>
)}
{repo} <Dot /> {deployment.envName}
{repo} <Dot /> {envName}
</>
}
sx={color ? sx : { bgcolor: "#1F1C3B", color: "white", ...sx }}
Expand Down
7 changes: 6 additions & 1 deletion src/shared/deployments/CommitInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ export default function CommitInfo({
},
},
}}
title={<AppChip deployment={deployment} />}
title={
<AppChip
repo={deployment.repo}
envName={deployment.envName}
/>
}
>
<Link
href={`/apps/${deployment.appId}/environments/${deployment.envId}/deployments`}
Expand Down
3 changes: 2 additions & 1 deletion src/shared/deployments/PublishModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export default function PublishModal({ deployment, onClose, onUpdate }: Props) {
}}
>
<AppChip
deployment={deployment}
repo={deployment.repo}
envName={deployment.envName}
sx={{ alignSelf: "flex-start", bgcolor: "transparent" }}
>
Publish to
Expand Down
26 changes: 26 additions & 0 deletions src/utils/helpers/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getLogoForProvider, parseRepo } from "./providers";

describe("~/utils/helpers/providers", () => {
test("parseRepo: parses the given repository as expected", () => {
expect(parseRepo("github/stormkit-io/app-stormkit-io")).toEqual({
provider: "github",
repo: "stormkit-io/app-stormkit-io",
});

expect(parseRepo("bitbucket/stormkit-io/app-stormkit-io")).toEqual({
provider: "bitbucket",
repo: "stormkit-io/app-stormkit-io",
});

expect(parseRepo("gitlab/stormkit-io/app-stormkit-io")).toEqual({
provider: "gitlab",
repo: "stormkit-io/app-stormkit-io",
});
});

test("getLogoForProvider: returns the correct logo for the provider", () => {
expect(getLogoForProvider("github")).toEqual("test-file-stub");
expect(getLogoForProvider("bitbucket")).toEqual("test-file-stub");
expect(getLogoForProvider("gitlab")).toEqual("test-file-stub");
});
});
Loading
Loading