Skip to content

Commit 49926d0

Browse files
selfcontainedroboquat
authored andcommitted
Adding org links and icons into org selector
1 parent 07c65aa commit 49926d0

File tree

9 files changed

+456
-155
lines changed

9 files changed

+456
-155
lines changed

components/dashboard/src/components/ContextMenu.tsx

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

7-
import React, { HTMLAttributeAnchorTarget } from "react";
7+
import React, { FunctionComponent, HTMLAttributeAnchorTarget } from "react";
88
import { useEffect, useState } from "react";
99
import { Link } from "react-router-dom";
1010
import cn from "classnames";
@@ -82,10 +82,6 @@ function ContextMenu(props: ContextMenuProps) {
8282
};
8383
}, []); // Empty array ensures that effect is only run on mount and unmount
8484

85-
const font = "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-100";
86-
87-
const menuId = String(Math.random());
88-
8985
// Default 'children' is the three dots hamburger button.
9086
const children = props.children || (
9187
<svg
@@ -118,7 +114,7 @@ function ContextMenu(props: ContextMenuProps) {
118114
</div>
119115
{expanded ? (
120116
<div
121-
className={`mt-2 z-50 bg-white dark:bg-gray-900 absolute flex flex-col border border-gray-200 dark:border-gray-800 rounded-lg truncated ${
117+
className={`cursor-default mt-2 z-50 bg-white dark:bg-gray-900 absolute flex flex-col border border-gray-200 dark:border-gray-800 rounded-lg truncated ${
122118
props.customClasses || "w-48 right-0"
123119
}`}
124120
data-analytics='{"button_type":"context_menu"}'
@@ -127,30 +123,14 @@ function ContextMenu(props: ContextMenuProps) {
127123
<p className="px-4 py-3">No actions available</p>
128124
) : (
129125
props.menuEntries.map((e, index) => {
130-
const clickable = e.href || e.onClick || e.link;
131126
const entry = (
132-
<div
133-
className={`px-4 flex py-3 ${
134-
clickable ? "hover:bg-gray-100 dark:hover:bg-gray-700" : ""
135-
} ${e.active ? "bg-gray-50 dark:bg-gray-800" : ""} ${
136-
index === 0 ? "rounded-t-lg" : ""
137-
} ${
138-
index === props.menuEntries.length - 1 ? "rounded-b-lg" : ""
139-
} text-sm leading-1 ${e.customFontStyle || font} ${
140-
e.separator ? " border-b border-gray-200 dark:border-gray-800" : ""
141-
}`}
142-
title={e.title}
143-
>
144-
{e.customContent || (
145-
<>
146-
<div className="truncate w-52">{e.title}</div>
147-
<div className="flex-1"></div>
148-
{e.active ? <div className="pl-1 font-semibold">&#x2713;</div> : null}
149-
</>
150-
)}
151-
</div>
127+
<MenuEntry
128+
{...e}
129+
isFirst={index === 0}
130+
isLast={index === props.menuEntries.length - 1}
131+
/>
152132
);
153-
const key = `entry-${menuId}-${index}-${e.title}`;
133+
const key = `entry-${index}-${e.title}`;
154134
if (e.link) {
155135
return (
156136
<Link key={key} to={e.link} onClick={e.onClick} target={e.target}>
@@ -185,3 +165,47 @@ function ContextMenu(props: ContextMenuProps) {
185165
}
186166

187167
export default ContextMenu;
168+
169+
type MenuEntryProps = ContextMenuEntry & {
170+
isFirst: boolean;
171+
isLast: boolean;
172+
};
173+
export const MenuEntry: FunctionComponent<MenuEntryProps> = ({
174+
title,
175+
href,
176+
link,
177+
active = false,
178+
separator = false,
179+
customContent,
180+
customFontStyle,
181+
isFirst,
182+
isLast,
183+
onClick,
184+
}) => {
185+
const clickable = href || link || onClick;
186+
187+
return (
188+
<div
189+
title={title}
190+
className={cn(
191+
"px-4 py-2 flex leading-1 text-sm",
192+
customFontStyle || "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-100",
193+
{
194+
"cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700": clickable,
195+
"bg-gray-50 dark:bg-gray-800": active,
196+
"rounded-t-lg": isFirst,
197+
"rounded-b-lg": isLast,
198+
"border-b border-gray-200 dark:border-gray-800": separator,
199+
},
200+
)}
201+
>
202+
{customContent || (
203+
<>
204+
<div className="truncate w-52">{title}</div>
205+
<div className="flex-1"></div>
206+
{active ? <div className="pl-1 font-semibold">&#x2713;</div> : null}
207+
</>
208+
)}
209+
</div>
210+
);
211+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 { FunctionComponent } from "react";
9+
import { consistentClassname } from "./consistent-classname";
10+
import "./styles.css";
11+
12+
const SIZE_CLASSES = {
13+
small: "w-6 h-6",
14+
medium: "w-10 h-10",
15+
};
16+
17+
const TEXT_SIZE_CLASSES = {
18+
small: "text-sm",
19+
medium: "text-xl",
20+
};
21+
22+
type Props = {
23+
id: string;
24+
name: string;
25+
size?: keyof typeof SIZE_CLASSES;
26+
className?: string;
27+
};
28+
export const OrgIcon: FunctionComponent<Props> = ({ id, name, size = "medium", className }) => {
29+
const logoBGClass = consistentClassname(id);
30+
const initials = getOrgInitials(name);
31+
const sizeClasses = SIZE_CLASSES[size];
32+
const textClass = TEXT_SIZE_CLASSES[size];
33+
34+
return (
35+
<div
36+
className={classNames("rounded-full flex items-center justify-center", sizeClasses, logoBGClass, className)}
37+
>
38+
<span className={`text-white font-semibold ${textClass}`}>{initials}</span>
39+
</div>
40+
);
41+
};
42+
43+
function getOrgInitials(name: string) {
44+
// If for some reason there is no name, default to G for Gitpod
45+
return (name || "G").charAt(0).toLocaleUpperCase();
46+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 { BG_CLASSES, consistentClassname } from "./consistent-classname";
8+
9+
describe("consistentClassname()", () => {
10+
test("empty string", () => {
11+
const id = "";
12+
const cn = consistentClassname(id);
13+
14+
expect(cn).toEqual(BG_CLASSES[0]);
15+
});
16+
17+
test("max value", () => {
18+
const id = "ffffffffffffffffffffffffffffffff";
19+
const cn = consistentClassname(id);
20+
21+
expect(cn).toEqual(BG_CLASSES[BG_CLASSES.length - 1]);
22+
});
23+
24+
test("with an id value", () => {
25+
const id = "c5895528-23ac-4ebd-9d8b-464228d5755f";
26+
const cn = consistentClassname(id);
27+
28+
expect(BG_CLASSES).toContain(cn);
29+
});
30+
31+
test("with an id value without hyphens", () => {
32+
const id = "c589552823ac4ebd9d8b464228d5755f";
33+
const cn = consistentClassname(id);
34+
35+
expect(BG_CLASSES).toContain(cn);
36+
});
37+
38+
test("with a shorter id value", () => {
39+
const id = "c5895528";
40+
const cn = consistentClassname(id);
41+
42+
expect(BG_CLASSES).toContain(cn);
43+
});
44+
45+
test("returns the same classname for the same value", () => {
46+
const id = "c5895528-23ac-4ebd-9d8b-464228d5755f";
47+
const cn1 = consistentClassname(id);
48+
const cn2 = consistentClassname(id);
49+
50+
expect(cn1).toEqual(cn2);
51+
expect(BG_CLASSES).toContain(cn1);
52+
expect(BG_CLASSES).toContain(cn2);
53+
});
54+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
// Represents the max value our base16 guid can be, 32 "f"s
8+
const GUID_MAX = "".padEnd(32, "f");
9+
10+
export const BG_CLASSES = [
11+
"bg-gradient-1",
12+
"bg-gradient-2",
13+
"bg-gradient-3",
14+
"bg-gradient-4",
15+
"bg-gradient-5",
16+
"bg-gradient-6",
17+
"bg-gradient-7",
18+
"bg-gradient-8",
19+
"bg-gradient-9",
20+
];
21+
22+
export const consistentClassname = (id: string) => {
23+
// Turn id into a 32 char. guid, pad with "0" if it's not 32 chars already
24+
const guid = id.replaceAll("-", "").substring(0, 32).padEnd(32, "0");
25+
26+
// Map guid into a 0,1 range by dividing by the max guid
27+
var quotient = parseInt(guid, 16) / parseInt(GUID_MAX, 16);
28+
var idx = Math.floor(quotient * (BG_CLASSES.length - 1));
29+
30+
return BG_CLASSES[idx];
31+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
8+
@layer components {
9+
.bg-gradient-1 {
10+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.orange.300') 75%);
11+
}
12+
.bg-gradient-2 {
13+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.pink.800') 75%);
14+
}
15+
.bg-gradient-3 {
16+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.red.500') 75%);
17+
}
18+
.bg-gradient-4 {
19+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.green.700') 75%);
20+
}
21+
.bg-gradient-5 {
22+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.yellow.300') 75%);
23+
}
24+
.bg-gradient-6 {
25+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.indigo.700') 75%);
26+
}
27+
.bg-gradient-7 {
28+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.teal.500') 75%);
29+
}
30+
.bg-gradient-8 {
31+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.sky.900') 75%);
32+
}
33+
.bg-gradient-9 {
34+
background: linear-gradient(45deg, theme('colors.orange.500') 25%, theme('colors.rose.300') 75%);
35+
}
36+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
8+
import { useQuery } from "@tanstack/react-query";
9+
import { getGitpodService } from "../../service/service";
10+
import { useCurrentUser } from "../../user-context";
11+
12+
type UserBillingModeQueryResult = BillingMode;
13+
14+
export const useUserBillingMode = () => {
15+
const user = useCurrentUser();
16+
17+
return useQuery<UserBillingModeQueryResult>({
18+
queryKey: getUserBillingModeQueryKey(user?.id ?? ""),
19+
queryFn: async () => {
20+
if (!user) {
21+
throw new Error("No current user, cannot load billing mode");
22+
}
23+
return await getGitpodService().server.getBillingModeForUser();
24+
},
25+
enabled: !!user,
26+
});
27+
};
28+
29+
export const getUserBillingModeQueryKey = (userId: string) => ["billing-mode", { userId }];

0 commit comments

Comments
 (0)