diff --git a/site/src/components/Timeline/Timeline.tsx b/site/src/components/Timeline/Timeline.tsx index ebd1ab9faaea0..28994e49ca26c 100644 --- a/site/src/components/Timeline/Timeline.tsx +++ b/site/src/components/Timeline/Timeline.tsx @@ -37,16 +37,12 @@ export const Timeline = ({ return ( <> - {Object.keys(itemsByDate).map((dateStr) => { - const items = itemsByDate[dateStr]; - - return ( - - - {items.map(row)} - - ); - })} + {Object.entries(itemsByDate).map(([dateStr, items]) => ( + + + {items.map(row)} + + ))} ); }; diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index 1279d4ca86890..fb43291dd48a6 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -233,21 +233,9 @@ const selectByLatency = ( return undefined; } - const proxyMap = proxies.reduce( - (acc, proxy) => { - acc[proxy.id] = proxy; - return acc; - }, - {} as Record, - ); - - const best = Object.keys(latencies) - .map((proxyId) => { - return { - id: proxyId, - ...latencies[proxyId], - }; - }) + const proxyMap = Object.fromEntries(proxies.map((it) => [it.id, it])); + const best = Object.entries(latencies) + .map(([proxyId, latency]) => ({ id: proxyId, ...latency })) // If the proxy is not in our list, or it is unhealthy, ignore it. .filter((latency) => proxyMap[latency.id]?.healthy) .sort((a, b) => a.latencyMS - b.latencyMS) diff --git a/site/src/modules/dashboard/entitlements.ts b/site/src/modules/dashboard/entitlements.ts index ebe9c074dc45f..601f2d6000ef1 100644 --- a/site/src/modules/dashboard/entitlements.ts +++ b/site/src/modules/dashboard/entitlements.ts @@ -13,12 +13,13 @@ export const getFeatureVisibility = ( return {}; } - const permissionPairs = Object.keys(features).map((feature) => { - const { entitlement, limit, actual, enabled } = features[feature]; - const entitled = ["entitled", "grace_period"].includes(entitlement); - const limitCompliant = limit && actual ? limit >= actual : true; - return [feature, entitled && limitCompliant && enabled]; - }); + const permissionPairs = Object.entries(features).map( + ([feature, { entitlement, limit, actual, enabled }]) => { + const entitled = ["entitled", "grace_period"].includes(entitlement); + const limitCompliant = limit && actual ? limit >= actual : true; + return [feature, entitled && limitCompliant && enabled]; + }, + ); return Object.fromEntries(permissionPairs); }; diff --git a/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx b/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx index 5d9b114dca73e..cfebbd81eee11 100644 --- a/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx +++ b/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx @@ -9,17 +9,23 @@ import { DockerIcon } from "components/Icons/DockerIcon"; import { type CSSProperties, type ElementType, type FC, useState } from "react"; import type { FileTree } from "utils/filetree"; -const sortFileTree = (fileTree: FileTree) => (a: string, b: string) => { - const contentA = fileTree[a]; - const contentB = fileTree[b]; - if (typeof contentA === "object") { - return -1; - } - if (typeof contentB === "object") { - return 1; +const isFolder = (content?: FileTree | string): content is FileTree => + typeof content === "object"; + +type FileTreeEntry = [key: string, content: FileTree | string]; +function compareFileTreeEntries( + [keyA, contentA]: FileTreeEntry, + [keyB, contentB]: FileTreeEntry, +) { + // A and B are either both files or both folders, so they should be sorted + // lexically. + if (isFolder(contentA) === isFolder(contentB)) { + return keyA.localeCompare(keyB); } - return a.localeCompare(b); -}; + // Either A or B is a folder, and the other is a file. Put whichever one is a + // folder first. + return isFolder(contentA) ? -1 : 1; +} type ContextMenu = { path: string; @@ -51,9 +57,6 @@ export const TemplateFileTree: FC = ({ }) => { const [contextMenu, setContextMenu] = useState(); - const isFolder = (content?: FileTree | string): content is FileTree => - typeof content === "object"; - const buildTreeItems = ( label: string, filename: string, @@ -181,12 +184,11 @@ export const TemplateFileTree: FC = ({ } > {isFolder(content) && - Object.keys(content) - .sort(sortFileTree(content)) - .map((filename) => { - const child = content[filename]; - return buildTreeItems(filename, filename, child, currentPath); - })} + Object.entries(content) + .sort(compareFileTreeEntries) + .map(([filename, child]) => + buildTreeItems(filename, filename, child, currentPath), + )} ); }; @@ -198,12 +200,9 @@ export const TemplateFileTree: FC = ({ defaultExpandedItems={activePath ? expandablePaths(activePath) : []} defaultSelectedItems={activePath} > - {Object.keys(fileTree) - .sort(sortFileTree(fileTree)) - .map((filename) => { - const child = fileTree[filename]; - return buildTreeItems(filename, filename, child); - })} + {Object.entries(fileTree) + .sort(compareFileTreeEntries) + .map(([filename, child]) => buildTreeItems(filename, filename, child))} setContextMenu(undefined)} diff --git a/site/src/modules/templates/TemplateFiles/TemplateFiles.tsx b/site/src/modules/templates/TemplateFiles/TemplateFiles.tsx index e803d6a01400b..95716e38827aa 100644 --- a/site/src/modules/templates/TemplateFiles/TemplateFiles.tsx +++ b/site/src/modules/templates/TemplateFiles/TemplateFiles.tsx @@ -2,7 +2,7 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import EditOutlined from "@mui/icons-material/EditOutlined"; import RadioButtonCheckedOutlined from "@mui/icons-material/RadioButtonCheckedOutlined"; import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"; -import set from "lodash/fp/set"; +import set from "lodash/set"; import { linkToTemplate, useLinks } from "modules/navigation"; import { type FC, useCallback, useMemo } from "react"; import { Link } from "react-router-dom"; @@ -31,8 +31,6 @@ export const TemplateFiles: FC = ({ const getLink = useLinks(); const theme = useTheme(); - const filenames = Object.keys(currentFiles); - const fileInfo = useCallback( (filename: string) => { const value = currentFiles[filename].trim(); @@ -49,13 +47,13 @@ export const TemplateFiles: FC = ({ ); const fileTree: FileTree = useMemo(() => { - let tree: FileTree = {}; - for (const filename of filenames) { + const tree: FileTree = {}; + for (const filename of Object.keys(currentFiles)) { const info = fileInfo(filename); - tree = set(filename.split("/"), info.value, tree); + set(tree, filename.split("/"), info.value); } return tree; - }, [fileInfo, filenames]); + }, [fileInfo, currentFiles]); const versionLink = `${getLink( linkToTemplate(organizationName, templateName), @@ -88,7 +86,7 @@ export const TemplateFiles: FC = ({
- {[...filenames] + {Object.keys(currentFiles) .sort((a, b) => a.localeCompare(b)) .map((filename) => { const info = fileInfo(filename); diff --git a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 8b120248771e4..004cf9716a44f 100644 --- a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -51,7 +51,6 @@ export const WorkspaceBuildLogs: FC = ({ }) => { const theme = useTheme(); const groupedLogsByStage = groupLogsByStage(logs); - const stages = Object.keys(groupedLogsByStage); return (
= ({ }} {...attrs} > - {stages.map((stage) => { - const logs = groupedLogsByStage[stage]; + {Object.entries(groupedLogsByStage).map(([stage, logs]) => { const isEmpty = logs.every((log) => log.output === ""); const lines = logs.map((log) => ({ time: log.created_at, diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index c2b3032ebede3..2c566500d892b 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -152,11 +152,9 @@ export const HealthLayout: FC = () => {