Skip to content

Commit bc8bd53

Browse files
Configuration Detail setup (#19021)
* Configuration Detail setup * nav * Breadcrumb above nav * type updates * Useconfig * Fix undefined vs null * Fix children rendering * Update colors * Cancel buttons * Delete configs * Refactor configuration detail page to use new RemoveConfiguration component * Fix styling and layout issues in BreadcrumbNav and RemoveConfiguration components * Update BreadcrumbNav and ConfigurationDetailPage components. * Fix spacings * Update BreadcrumbNav styling * Containerify * Update configuration name and style, and add dark mode support. * Update BreadcrumbNav styling to include dark mode hover text color * Fix text color in BreadcrumbNav component * Add WidePageWithSubMenu component and export SubmenuItem * Revert `PageWithSubMenu` changes * Fix types * Unify margins for ConfigurationName * Icons * Refactor configuration deletion queries Co-authored-by: Brad Harris <[email protected]> * Less useStates :} Co-authored-by: Brad Harris <[email protected]> * Call `onRemoved` only at `onSuccess` * Add MiddleDot component to BreadcrumbNav Co-authored-by: Brad Harris <[email protected]> * Fix rectangular focus indicators * Add icons to configurations menu * navs * Conditionally render children * Fix rectangular list items * `configurations` -> `repositories` underneath 🫣 * Update links in OrganizationSelector and ConfigurationDetailPage * Update BreadcrumbNav to use larger font size for page description * Componentize config setting fields * y margin * Fix contrast for Confirmation modal descriptions * use dirty state hook and fix cancelling after updating * Re-structure * `asChild` for podkit headings * Fix save button disabled state in ConfigurationNameForm component * Add type attribute to button in ConfigurationNameForm component * Remove hack, which didn't work 🙃 * Fix error handling in ConfigurationDetailPage component * Refactor sub-menu item styles to improve accessibility. * Update ConfigurationNameForm component with useState hook * Do not memoize 🤖 --------- Co-authored-by: Brad Harris <[email protected]>
1 parent aa691dd commit bc8bd53

18 files changed

+493
-156
lines changed

components/dashboard/src/app/AppRoutes.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ const WorkspacesSearch = React.lazy(() => import(/* webpackPrefetch: true */ "..
7676
const ProjectsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "../admin/ProjectsSearch"));
7777
const TeamsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "../admin/TeamsSearch"));
7878
const Usage = React.lazy(() => import(/* webpackPrefetch: true */ "../Usage"));
79-
const RepositoryListPage = React.lazy(() => import(/* webpackPrefetch: true */ "../repositories/list/RepositoryList"));
80-
const RepositoryDetailPage = React.lazy(
81-
() => import(/* webpackPrefetch: true */ "../repositories/detail/RepositoryDetail"),
79+
const ConfigurationListPage = React.lazy(
80+
() => import(/* webpackPrefetch: true */ "../repositories/list/RepositoryList"),
81+
);
82+
const ConfigurationGeneral = React.lazy(
83+
() => import(/* webpackPrefetch: true */ "../repositories/detail/ConfigurationDetailGeneral"),
8284
);
8385

8486
export const AppRoutes = () => {
@@ -216,8 +218,8 @@ export const AppRoutes = () => {
216218

217219
{repoConfigListAndDetail && (
218220
<>
219-
<Route exact path="/repositories" component={RepositoryListPage} />
220-
<Route exact path="/repositories/:id" component={RepositoryDetailPage} />
221+
<Route exact path="/repositories" component={ConfigurationListPage} />
222+
<Route exact path="/repositories/:id" component={ConfigurationGeneral} />
221223
</>
222224
)}
223225
{/* basic redirect for old team slugs */}

components/dashboard/src/components/ConfirmationModal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export const ConfirmationModal: FC<Props> = ({
6666
{isEntity(children) ? (
6767
<div className="w-full p-4 mb-2 bg-gray-100 dark:bg-gray-700 rounded-xl group">
6868
<p className="text-base text-gray-800 dark:text-gray-100 font-semibold">{children.name}</p>
69-
{children.description && <p className="text-gray-500 truncate">{children.description}</p>}
69+
{children.description && (
70+
<p className="text-gray-500 dark:text-gray-300 truncate">{children.description}</p>
71+
)}
7072
</div>
7173
) : (
7274
children

components/dashboard/src/components/PageWithSubMenu.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ import { Separator } from "./Separator";
1414
export interface PageWithSubMenuProps {
1515
title: string;
1616
subtitle: string;
17-
subMenu: {
18-
title: string;
19-
link: string[];
20-
}[];
17+
subMenu: SubmenuItemProps[];
2118
tabs?: TabEntry[];
2219
children: React.ReactNode;
2320
}
@@ -53,12 +50,13 @@ export function PageWithSubMenu(p: PageWithSubMenuProps) {
5350
);
5451
}
5552

56-
type SubmenuItemProps = {
53+
export type SubmenuItemProps = {
5754
title: string;
5855
link: string[];
56+
icon?: React.ReactNode;
5957
};
6058

61-
const SubmenuItem: FC<SubmenuItemProps> = ({ title, link }) => {
59+
export const SubmenuItem: FC<SubmenuItemProps> = ({ title, link, icon }) => {
6260
const location = useLocation();
6361
const itemRef = useRef<HTMLLIElement>(null);
6462

@@ -69,17 +67,19 @@ const SubmenuItem: FC<SubmenuItemProps> = ({ title, link }) => {
6967
}
7068
}, [link, location.pathname]);
7169

72-
let classes = "flex block py-2 px-4 rounded-md whitespace-nowrap max-w-52";
70+
let classes = "flex justify-between block rounded-md py-2 px-4 whitespace-nowrap max-w-52";
7371

74-
if (link.some((l) => l === location.pathname)) {
72+
const isCurrent = link.some((l) => l === location.pathname);
73+
if (isCurrent) {
7574
classes += " bg-gray-300 text-gray-800 dark:bg-gray-800 dark:text-gray-50";
7675
} else {
77-
classes += " hover:bg-gray-100 dark:hover:bg-gray-800";
76+
classes += " hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-400";
7877
}
78+
7979
return (
80-
<Link to={link[0]} key={title} className="md:w-full">
80+
<Link to={link[0]} key={title} className="md:w-full rounded-md">
8181
<li ref={itemRef} className={classes}>
82-
{title}
82+
{title} {icon}
8383
</li>
8484
</Link>
8585
);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import classNames from "classnames";
8+
import { Separator } from "./Separator";
9+
import { cn } from "@podkit/lib/cn";
10+
import { SubmenuItem, SubmenuItemProps } from "./PageWithSubMenu";
11+
12+
export interface PageWithSubMenuProps {
13+
/**
14+
* The name of the navigation menu, as read by screen readers.
15+
*/
16+
navTitle?: string;
17+
subMenu: SubmenuItemProps[];
18+
children: React.ReactNode;
19+
}
20+
21+
export function WidePageWithSubMenu(p: PageWithSubMenuProps) {
22+
return (
23+
<div className="w-full">
24+
<div className={cn("app-container flex flex-col md:flex-row")}>
25+
{/* TODO: extract into SubMenu component and show scrolling indicators */}
26+
<nav aria-label={p.navTitle}>
27+
<ul
28+
className={classNames(
29+
// Handle flipping between row and column layout
30+
"flex flex-row md:flex-col items-center",
31+
"w-full md:w-52 overflow-auto md:overflow-visible",
32+
"pt-4 pb-4 md:pb-0",
33+
"space-x-2 md:space-x-0 md:space-y-2",
34+
"tracking-wide text-gray-500",
35+
)}
36+
>
37+
{p.subMenu.map((e) => {
38+
return <SubmenuItem title={e.title} link={e.link} key={e.title} icon={e.icon} />;
39+
})}
40+
</ul>
41+
</nav>
42+
<div className="md:ml-4 w-full pt-1 mb-40">
43+
<Separator className="md:hidden" />
44+
<div className="pt-4 md:pt-0">{p.children}</div>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { LinkButton } from "@podkit/buttons/LinkButton";
8+
import { cn } from "@podkit/lib/cn";
9+
import { ChevronLeft } from "lucide-react";
10+
import type { FC } from "react";
11+
import { MiddleDot } from "../../typography/MiddleDot";
12+
import { Heading3 } from "@podkit/typography/Headings";
13+
14+
interface BreadcrumbPageNavProps {
15+
/**
16+
* The title of the current page.
17+
*/
18+
pageTitle: string;
19+
/**
20+
* The description of the current page.
21+
*/
22+
pageDescription?: string;
23+
/**
24+
* The link to the previous page.
25+
*/
26+
backLink?: string;
27+
className?: string;
28+
}
29+
30+
export const BreadcrumbNav: FC<BreadcrumbPageNavProps> = ({ pageTitle, pageDescription, backLink, className }) => {
31+
return (
32+
<section className={cn("flex flex-row items-center justify-start gap-2 w-full py-4 app-container", className)}>
33+
{backLink && (
34+
<LinkButton
35+
variant={"ghost"}
36+
className="py-1 px-0 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-gray-200"
37+
href={backLink}
38+
>
39+
<ChevronLeft size={24} />
40+
</LinkButton>
41+
)}
42+
<Heading3 asChild>
43+
<h1>{pageTitle}</h1>
44+
</Heading3>
45+
<MiddleDot />
46+
<p className="text-gray-900 dark:text-gray-300 text-lg">{pageDescription}</p>
47+
</section>
48+
);
49+
};

components/dashboard/src/components/podkit/typography/Headings.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,49 @@
66

77
import { cn } from "@podkit/lib/cn";
88
import { FC } from "react";
9+
import { Slot } from "@radix-ui/react-slot";
910

1011
type HeadingProps = {
1112
id?: string;
1213
tracking?: "tight" | "wide";
1314
color?: "light" | "dark";
1415
className?: string;
16+
asChild?: boolean;
1517
};
1618

17-
export const Heading1: FC<HeadingProps> = ({ id, color, tracking, className, children }) => {
19+
export const Heading1: FC<HeadingProps> = ({ id, color, tracking, className, children, asChild }) => {
20+
const Comp = asChild ? Slot : "h1";
21+
1822
return (
19-
<h1
23+
<Comp
2024
id={id}
2125
className={cn(getHeadingColor(color), getTracking(tracking), "font-bold text-4xl truncate", className)}
2226
>
2327
{children}
24-
</h1>
28+
</Comp>
2529
);
2630
};
2731

28-
export const Heading2: FC<HeadingProps> = ({ id, color, tracking, className, children }) => {
32+
export const Heading2: FC<HeadingProps> = ({ id, color, tracking, className, children, asChild }) => {
33+
const Comp = asChild ? Slot : "h2";
34+
2935
return (
30-
<h2 id={id} className={cn(getHeadingColor(color), getTracking(tracking), "font-semibold text-2xl", className)}>
36+
<Comp
37+
id={id}
38+
className={cn(getHeadingColor(color), getTracking(tracking), "font-semibold text-2xl", className)}
39+
>
3140
{children}
32-
</h2>
41+
</Comp>
3342
);
3443
};
3544

36-
export const Heading3: FC<HeadingProps> = ({ id, color, tracking, className, children }) => {
45+
export const Heading3: FC<HeadingProps> = ({ id, color, tracking, className, children, asChild }) => {
46+
const Comp = asChild ? Slot : "h3";
47+
3748
return (
38-
<h3 id={id} className={cn(getHeadingColor(color), getTracking(tracking), "font-semibold text-lg", className)}>
49+
<Comp id={id} className={cn(getHeadingColor(color), getTracking(tracking), "font-semibold text-lg", className)}>
3950
{children}
40-
</h3>
51+
</Comp>
4152
);
4253
};
4354

components/dashboard/src/data/configurations/configuration-queries.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { useQuery } from "@tanstack/react-query";
7+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
88
import { useCurrentOrg } from "../organizations/orgs-query";
99
import { configurationClient } from "../../service/public-api";
10-
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
10+
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
1111

1212
const BASE_KEY = "configurations";
1313

@@ -60,6 +60,25 @@ export const useConfiguration = (configurationId: string) => {
6060
});
6161
};
6262

63+
type DeleteConfigurationArgs = {
64+
configurationId: string;
65+
};
66+
export const useDeleteConfiguration = () => {
67+
const queryClient = useQueryClient();
68+
69+
return useMutation({
70+
mutationFn: async ({ configurationId }: DeleteConfigurationArgs) => {
71+
return await configurationClient.deleteConfiguration({
72+
configurationId,
73+
});
74+
},
75+
onSuccess: (_, { configurationId }) => {
76+
queryClient.invalidateQueries({ queryKey: ["configurations", "list"] });
77+
queryClient.invalidateQueries({ queryKey: getConfigurationQueryKey(configurationId) });
78+
},
79+
});
80+
};
81+
6382
export const getConfigurationQueryKey = (configurationId: string) => {
6483
const key: any[] = [BASE_KEY, { configurationId }];
6584

components/dashboard/src/menu/OrganizationSelector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export default function OrganizationSelector() {
5757
if (currentOrg.data) {
5858
if (repoConfigListAndDetail) {
5959
linkEntries.push({
60-
title: "Repositories",
61-
customContent: <LinkEntry>Repositories</LinkEntry>,
60+
title: "Configurations",
61+
customContent: <LinkEntry>Configurations</LinkEntry>,
6262
active: false,
6363
separator: false,
6464
link: "/repositories",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { FC } from "react";
8+
import { useParams } from "react-router";
9+
import { ConfigurationNameForm } from "./general/ConfigurationName";
10+
import { ConfigurationDetailPage } from "./ConfigurationDetailPage";
11+
import { useConfiguration } from "../../data/configurations/configuration-queries";
12+
import { RemoveConfiguration } from "./general/RemoveConfiguration";
13+
14+
type PageRouteParams = {
15+
id: string;
16+
};
17+
const ConfigurationDetailGeneral: FC = () => {
18+
const { id } = useParams<PageRouteParams>();
19+
const configurationQuery = useConfiguration(id);
20+
const { data } = configurationQuery;
21+
22+
return (
23+
<ConfigurationDetailPage configurationQuery={configurationQuery} id={id}>
24+
{data && (
25+
<>
26+
<ConfigurationNameForm configuration={data} />
27+
<RemoveConfiguration configuration={data} />
28+
</>
29+
)}
30+
</ConfigurationDetailPage>
31+
);
32+
};
33+
34+
export default ConfigurationDetailGeneral;

0 commit comments

Comments
 (0)