Skip to content

feat: add /icons page #10093

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

Merged
merged 2 commits into from
Oct 9, 2023
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 site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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",
Expand Down
7 changes: 7 additions & 0 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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">
Expand Down Expand Up @@ -261,7 +262,7 @@ export const AppRouter: FC = () => {
</Route>
</Route>

<Route path="users">
<Route path="/users">
<Route element={<UsersLayout />}>
<Route index element={<UsersPage />} />
</Route>
Expand Down Expand Up @@ -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 />} />
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions site/src/components/CopyableValue/CopyableValue.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
196 changes: 196 additions & 0 deletions site/src/pages/IconsPage/IconsPage.tsx
Original file line number Diff line number Diff line change
@@ -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;