fix(site): resolve all Array.prototype.toSorted and Array.prototype.sort bugs (#17307)

Closes https://github.com/coder/coder/issues/16759

## Changes made
- Replaced all instances of `Array.prototype.toSorted` with
`Array.prototype.sort` to provide better support for older browsers
- Updated all `Array.prototype.sort` calls where necessary to remove
risks of mutation render bugs
- Refactored some code (moved things around, added comments) to make it
more clear that certain `.sort` calls are harmless and don't have any
risks
This commit is contained in:
Michael Smith
2025-04-09 15:17:02 -04:00
committed by GitHub
parent d17bcc727b
commit a03a54dd14
9 changed files with 87 additions and 88 deletions
+2 -2
View File
@@ -270,7 +270,7 @@ export const organizationsPermissions = (
}
return {
queryKey: ["organizations", organizationIds.sort(), "permissions"],
queryKey: ["organizations", [...organizationIds.sort()], "permissions"],
queryFn: async () => {
// Only request what we need for the sidebar, which is one edit permission
// per sub-link (settings, groups, roles, and members pages) that tells us
@@ -316,7 +316,7 @@ export const workspacePermissionsByOrganization = (
}
return {
queryKey: ["workspaces", organizationIds.sort(), "permissions"],
queryKey: ["workspaces", [...organizationIds.sort()], "permissions"],
queryFn: async () => {
const prefixedChecks = organizationIds.flatMap((orgId) =>
Object.entries(workspacePermissionChecks(orgId, userId)).map(
@@ -4,7 +4,7 @@ export function sortProxiesByLatency(
proxies: Proxies,
latencies: ProxyLatencies,
) {
return proxies.toSorted((a, b) => {
return [...proxies].sort((a, b) => {
const latencyA = latencies?.[a.id]?.latencyMS ?? Number.POSITIVE_INFINITY;
const latencyB = latencies?.[b.id]?.latencyMS ?? Number.POSITIVE_INFINITY;
return latencyA - latencyB;
@@ -13,9 +13,9 @@ export const mergeTimeRanges = (ranges: TimeRange[]): TimeRange => {
.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
const start = sortedDurations[0].startedAt;
const sortedEndDurations = ranges
.slice()
.sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime());
const sortedEndDurations = [...ranges].sort(
(a, b) => a.endedAt.getTime() - b.endedAt.getTime(),
);
const end = sortedEndDurations[sortedEndDurations.length - 1].endedAt;
return { startedAt: start, endedAt: end };
};
@@ -26,7 +26,7 @@ const sortVisibleTemplates = (templates: TemplateExample[]) => {
// The docker template should be the first template in the list,
// as it's the easiest way to get started with Coder.
const dockerTemplateId = "docker";
return templates.sort((a, b) => {
return [...templates].sort((a, b) => {
if (a.id === dockerTemplateId) {
return -1;
}
@@ -99,7 +99,7 @@ const LicensesSettingsPageView: FC<Props> = ({
{!isLoading && licenses && licenses?.length > 0 && (
<Stack spacing={4} className="licenses">
{licenses
{[...(licenses ?? [])]
?.sort(
(a, b) =>
new Date(b.claims.license_expires).valueOf() -
+1 -1
View File
@@ -91,7 +91,7 @@ export const DERPPage: FC = () => {
<section>
<SectionLabel>Regions</SectionLabel>
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
{Object.values(regions!)
{Object.values(regions ?? {})
.filter((region) => {
// Values can technically be null
return region !== null;
@@ -170,8 +170,8 @@ const RoleTable: FC<RoleTableProps> = ({
</Cond>
<Cond>
{roles
?.sort((a, b) => a.name.localeCompare(b.name))
{[...(roles ?? [])]
.sort((a, b) => a.name.localeCompare(b.name))
.map((role) => (
<RoleRow
key={role.name}
@@ -412,7 +412,9 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
...panelProps
}) => {
const theme = useTheme();
const validUsage = data?.filter((u) => u.seconds > 0);
const validUsage = data
?.filter((u) => u.seconds > 0)
.sort((a, b) => b.seconds - a.seconds);
const totalInSeconds =
validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1;
const usageColors = chroma
@@ -438,86 +440,82 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
gap: 24,
}}
>
{validUsage
.sort((a, b) => b.seconds - a.seconds)
.map((usage, i) => {
const percentage = (usage.seconds / totalInSeconds) * 100;
return (
<div
key={usage.slug}
css={{ display: "flex", gap: 24, alignItems: "center" }}
>
{validUsage.map((usage, i) => {
const percentage = (usage.seconds / totalInSeconds) * 100;
return (
<div
key={usage.slug}
css={{ display: "flex", gap: 24, alignItems: "center" }}
>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
<div
css={{ display: "flex", alignItems: "center", gap: 8 }}
>
<div
css={{
width: 20,
height: 20,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<img
src={usage.icon}
alt=""
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</div>
<div css={{ fontSize: 13, fontWeight: 500, width: 200 }}>
{usage.display_name}
</div>
</div>
<Tooltip
title={`${Math.floor(percentage)}%`}
placement="top"
arrow
>
<LinearProgress
value={percentage}
variant="determinate"
css={{
width: "100%",
height: 8,
backgroundColor: theme.palette.divider,
"& .MuiLinearProgress-bar": {
backgroundColor: usageColors[i],
borderRadius: 999,
},
}}
/>
</Tooltip>
<Stack
spacing={0}
css={{
fontSize: 13,
color: theme.palette.text.secondary,
width: 120,
flexShrink: 0,
lineHeight: "1.5",
width: 20,
height: 20,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{formatTime(usage.seconds)}
{usage.times_used > 0 && (
<span
css={{
fontSize: 12,
color: theme.palette.text.disabled,
}}
>
Opened {usage.times_used.toLocaleString()}{" "}
{usage.times_used === 1 ? "time" : "times"}
</span>
)}
</Stack>
<img
src={usage.icon}
alt=""
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</div>
<div css={{ fontSize: 13, fontWeight: 500, width: 200 }}>
{usage.display_name}
</div>
</div>
);
})}
<Tooltip
title={`${Math.floor(percentage)}%`}
placement="top"
arrow
>
<LinearProgress
value={percentage}
variant="determinate"
css={{
width: "100%",
height: 8,
backgroundColor: theme.palette.divider,
"& .MuiLinearProgress-bar": {
backgroundColor: usageColors[i],
borderRadius: 999,
},
}}
/>
</Tooltip>
<Stack
spacing={0}
css={{
fontSize: 13,
color: theme.palette.text.secondary,
width: 120,
flexShrink: 0,
lineHeight: "1.5",
}}
>
{formatTime(usage.seconds)}
{usage.times_used > 0 && (
<span
css={{
fontSize: 12,
color: theme.palette.text.disabled,
}}
>
Opened {usage.times_used.toLocaleString()}{" "}
{usage.times_used === 1 ? "time" : "times"}
</span>
)}
</Stack>
</div>
);
})}
</div>
)}
</PanelContent>
+2 -1
View File
@@ -165,7 +165,8 @@ export const AppStatuses: FC<AppStatusesProps> = ({
})),
);
// 2. Sort statuses chronologically (newest first)
// 2. Sort statuses chronologically (newest first) - mutating the value is
// fine since it's not an outside parameter
allStatuses.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),