mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add /icons page (#10093)
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+7
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user