feat: add /icons page (#10093)

This commit is contained in:
Kayla Washburn
2023-10-09 09:49:26 -06:00
committed by GitHub
parent 236e84c4d6
commit bda68b143a
5 changed files with 226 additions and 12 deletions
+1
View File
@@ -91,6 +91,7 @@
"ts-prune": "0.10.3",
"tzdata": "1.0.30",
"ua-parser-js": "1.0.33",
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
"unique-names-generator": "4.7.1",
"uuid": "9.0.0",
"vite": "4.4.2",
+7
View File
@@ -189,6 +189,9 @@ dependencies:
ua-parser-js:
specifier: 1.0.33
version: 1.0.33
ufuzzy:
specifier: npm:@leeoniya/ufuzzy@1.0.10
version: /@leeoniya/ufuzzy@1.0.10
unique-names-generator:
specifier: 4.7.1
version: 4.7.1
@@ -3177,6 +3180,10 @@ packages:
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
dev: false
/@leeoniya/ufuzzy@1.0.10:
resolution: {integrity: sha512-OR1yiyN8cKBn5UiHjKHUl0LcrTQt4vZPUpIf96qIIZVLxgd4xyASuRvTZ3tjbWvuyQAMgvKsq61Nwu131YyHnA==}
dev: false
/@mapbox/node-pre-gyp@1.0.11:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
+10 -8
View File
@@ -193,6 +193,7 @@ const TemplateInsightsPage = lazy(
);
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"));
const GroupsPage = lazy(() => import("./pages/GroupsPage/GroupsPage"));
const IconsPage = lazy(() => import("./pages/IconsPage/IconsPage"));
export const AppRouter: FC = () => {
return (
@@ -207,21 +208,21 @@ export const AppRouter: FC = () => {
<Route element={<DashboardLayout />}>
<Route index element={<Navigate to="/workspaces" replace />} />
<Route path="health" element={<HealthPage />} />
<Route path="/health" element={<HealthPage />} />
<Route
path="external-auth/:provider"
path="/external-auth/:provider"
element={<ExternalAuthPage />}
/>
<Route path="workspaces" element={<WorkspacesPage />} />
<Route path="/workspaces" element={<WorkspacesPage />} />
<Route path="starter-templates">
<Route path="/starter-templates">
<Route index element={<StarterTemplatesPage />} />
<Route path=":exampleId" element={<StarterTemplatePage />} />
</Route>
<Route path="templates">
<Route path="/templates">
<Route index element={<TemplatesPage />} />
<Route path="new" element={<CreateTemplatePage />} />
<Route path=":template">
@@ -261,7 +262,7 @@ export const AppRouter: FC = () => {
</Route>
</Route>
<Route path="users">
<Route path="/users">
<Route element={<UsersLayout />}>
<Route index element={<UsersPage />} />
</Route>
@@ -302,7 +303,7 @@ export const AppRouter: FC = () => {
/>
</Route>
<Route path="settings" element={<SettingsLayout />}>
<Route path="/settings" element={<SettingsLayout />}>
<Route path="account" element={<AccountPage />} />
<Route path="schedule" element={<SchedulePage />} />
<Route path="security" element={<SecurityPage />} />
@@ -340,7 +341,8 @@ export const AppRouter: FC = () => {
path="/:username/:workspace/terminal"
element={<TerminalPage renderer="webgl" />}
/>
<Route path="cli-auth" element={<CliAuthenticationPage />} />
<Route path="/cli-auth" element={<CliAuthenticationPage />} />
<Route path="/icons" element={<IconsPage />} />
</Route>
{/* Using path="*"" means "match anything", so this route
@@ -1,20 +1,28 @@
import Tooltip from "@mui/material/Tooltip";
import Tooltip, { type TooltipProps } from "@mui/material/Tooltip";
import { useClickable } from "hooks/useClickable";
import { useClipboard } from "hooks/useClipboard";
import { FC, HTMLProps } from "react";
import { type FC, type HTMLProps } from "react";
interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
value: string;
placement?: TooltipProps["placement"];
PopperProps?: TooltipProps["PopperProps"];
}
export const CopyableValue: FC<CopyableValueProps> = ({ value, ...props }) => {
export const CopyableValue: FC<CopyableValueProps> = ({
value,
placement = "bottom-start",
PopperProps,
...props
}) => {
const { isCopied, copy } = useClipboard(value);
const clickableProps = useClickable<HTMLSpanElement>(copy);
return (
<Tooltip
title={isCopied ? "Copied!" : "Click to copy"}
placement="bottom-start"
placement={placement}
PopperProps={PopperProps}
>
<span {...props} {...clickableProps} css={{ cursor: "pointer" }} />
</Tooltip>
+196
View File
@@ -0,0 +1,196 @@
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import ClearIcon from "@mui/icons-material/CloseOutlined";
import { useTheme } from "@emotion/react";
import { type FC, type ReactNode, useMemo, useState } from "react";
import { Helmet } from "react-helmet-async";
import uFuzzy from "ufuzzy";
import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Margins } from "components/Margins/Margins";
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import icons from "theme/icons.json";
import { pageTitle } from "utils/page";
const iconsWithoutSuffix = icons.map((icon) => icon.split(".")[0]);
const fuzzyFinder = new uFuzzy({
intraMode: 1,
intraIns: 1,
intraSub: 1,
intraTrn: 1,
intraDel: 1,
});
export const IconsPage: FC = () => {
const theme = useTheme();
const [searchInputText, setSearchInputText] = useState("");
const searchText = searchInputText.trim();
const searchedIcons = useMemo(() => {
if (!searchText) {
return icons.map((icon) => ({ url: `/icon/${icon}`, description: icon }));
}
const [map, info, sorted] = fuzzyFinder.search(
iconsWithoutSuffix,
searchText,
);
// We hit an invalid state somehow
if (!map || !info || !sorted) {
return [];
}
return sorted.map((i) => {
const iconName = icons[info.idx[i]];
const ranges = info.ranges[i];
const nodes: ReactNode[] = [];
let cursor = 0;
for (let j = 0; j < ranges.length; j += 2) {
nodes.push(iconName.slice(cursor, ranges[j]));
nodes.push(
<mark key={j + 1}>{iconName.slice(ranges[j], ranges[j + 1])}</mark>,
);
cursor = ranges[j + 1];
}
nodes.push(iconName.slice(cursor));
return { url: `/icon/${iconName}`, description: nodes };
});
}, [searchText]);
return (
<>
<Helmet>
<title>{pageTitle("Icons")}</title>
</Helmet>
<Margins>
<PageHeader
actions={
<Tooltip
placement="bottom-end"
title={
<Box
css={{
padding: theme.spacing(1),
fontSize: 13,
lineHeight: 1.5,
}}
>
You can suggest a new icon by submitting a Pull Request to our
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
permissive license.
</Box>
}
>
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
Suggest an icon
</Link>
</Tooltip>
}
>
<PageHeaderTitle>Icons</PageHeaderTitle>
<PageHeaderSubtitle>
All of the icons included with Coder
</PageHeaderSubtitle>
</PageHeader>
<TextField
size="small"
InputProps={{
"aria-label": "Filter",
name: "query",
placeholder: "Search…",
value: searchInputText,
onChange: (event) => setSearchInputText(event.target.value),
sx: {
borderRadius: "6px",
marginLeft: "-1px",
"& input::placeholder": {
color: theme.palette.text.secondary,
},
"& .MuiInputAdornment-root": {
marginLeft: 0,
},
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
}}
/>
</InputAdornment>
),
endAdornment: searchInputText && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => setSearchInputText("")}
>
<ClearIcon sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
<Stack
direction="row"
wrap="wrap"
spacing={1}
justifyContent="center"
css={(theme) => ({ marginTop: theme.spacing(4) })}
>
{searchedIcons.length === 0 && (
<EmptyState message="No results matched your search" />
)}
{searchedIcons.map((icon) => (
<CopyableValue key={icon.url} value={icon.url} placement="bottom">
<Stack alignItems="center" css={{ margin: theme.spacing(1.5) }}>
<img
alt={icon.url}
src={icon.url}
css={{
width: 60,
height: 60,
objectFit: "contain",
pointerEvents: "none",
padding: theme.spacing(1.5),
}}
/>
<figcaption
css={{
width: 88,
height: 48,
fontSize: 13,
textOverflow: "ellipsis",
textAlign: "center",
overflow: "hidden",
}}
>
{icon.description}
</figcaption>
</Stack>
</CopyableValue>
))}
</Stack>
</Margins>
</>
);
};
export default IconsPage;