mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: minor task page design adjustments (#18187)
**Before:** <img width="1624" alt="Screenshot 2025-06-02 at 13 38 45" src="https://github.com/user-attachments/assets/545400e9-18d6-44a3-9f21-a51ceceedc8e" /> **After:** <img width="1624" alt="Screenshot 2025-06-02 at 13 40 18" src="https://github.com/user-attachments/assets/1b0b89f0-8cd6-4afd-8800-786e43ae4b52" /> - Show output links - Move workspace app bar to top of screen - Remove redundant task status indicators - Update "building your task" to "Building your workspace" to be more transparent - Change “Open IDE” language to “Open Locally” Closes: - https://github.com/coder/coder/issues/18181 - https://github.com/coder/coder/issues/18162
This commit is contained in:
+13
-8
@@ -1,6 +1,7 @@
|
||||
import type { WorkspaceAppStatus } from "api/typesGenerated";
|
||||
import type { WorkspaceAppStatusState } from "api/typesGenerated";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import {
|
||||
BanIcon,
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
HourglassIcon,
|
||||
@@ -9,20 +10,22 @@ import {
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
type AppStatusIconProps = {
|
||||
status: WorkspaceAppStatus;
|
||||
type AppStatusStateIconProps = {
|
||||
state: WorkspaceAppStatusState;
|
||||
latest: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const AppStatusIcon: FC<AppStatusIconProps> = ({
|
||||
status,
|
||||
export const AppStatusStateIcon: FC<AppStatusStateIconProps> = ({
|
||||
state,
|
||||
disabled,
|
||||
latest,
|
||||
className: customClassName,
|
||||
}) => {
|
||||
const className = cn(["size-4 shrink-0", customClassName]);
|
||||
|
||||
switch (status.state) {
|
||||
switch (state) {
|
||||
case "complete":
|
||||
return (
|
||||
<CircleCheckIcon className={cn(["text-content-success", className])} />
|
||||
@@ -32,10 +35,12 @@ export const AppStatusIcon: FC<AppStatusIconProps> = ({
|
||||
<CircleAlertIcon className={cn(["text-content-warning", className])} />
|
||||
);
|
||||
case "working":
|
||||
return latest ? (
|
||||
return disabled ? (
|
||||
<BanIcon className={cn(["text-content-disabled", className])} />
|
||||
) : latest ? (
|
||||
<Spinner size="sm" className="shrink-0" loading />
|
||||
) : (
|
||||
<HourglassIcon className={cn(["text-highlight-sky", className])} />
|
||||
<HourglassIcon className={cn(["text-content-secondary", className])} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { AppStatusIcon } from "modules/apps/AppStatusIcon";
|
||||
import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
type WorkspaceAppStatusProps = {
|
||||
@@ -31,9 +31,10 @@ export const WorkspaceAppStatus = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppStatusIcon
|
||||
status={status}
|
||||
<AppStatusStateIcon
|
||||
latest
|
||||
disabled={disabled}
|
||||
state={status.state}
|
||||
className={cn({
|
||||
"text-content-disabled": disabled,
|
||||
})}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import GitHub from "@mui/icons-material/GitHub";
|
||||
import { API } from "api/api";
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import type { WorkspaceApp, WorkspaceStatus } from "api/typesGenerated";
|
||||
@@ -21,14 +22,17 @@ import {
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
BugIcon,
|
||||
ChevronDownIcon,
|
||||
EllipsisVerticalIcon,
|
||||
ExternalLinkIcon,
|
||||
GitPullRequestArrowIcon,
|
||||
LayoutGridIcon,
|
||||
RotateCcwIcon,
|
||||
} from "lucide-react";
|
||||
import { AppStatusIcon } from "modules/apps/AppStatusIcon";
|
||||
import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon";
|
||||
import { useAppLink } from "modules/apps/useAppLink";
|
||||
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
|
||||
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
|
||||
import type React from "react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
@@ -38,6 +42,7 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { cn } from "utils/cn";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { timeFrom } from "utils/time";
|
||||
import { truncateURI } from "utils/uri";
|
||||
|
||||
const TaskPage = () => {
|
||||
const { workspace: workspaceName, username } = useParams() as {
|
||||
@@ -115,10 +120,10 @@ const TaskPage = () => {
|
||||
<div className="flex flex-col items-center">
|
||||
<Spinner loading className="mb-4" />
|
||||
<h3 className="m-0 font-medium text-content-primary text-base">
|
||||
Building your task
|
||||
Building the workspace
|
||||
</h3>
|
||||
<span className="text-content-secondary text-sm">
|
||||
Your task is being built and will be ready soon
|
||||
Your task will run as soon as the workspace is ready
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,21 +151,21 @@ const TaskPage = () => {
|
||||
} else if (terminatedStatuses.includes(task.workspace.latest_build.status)) {
|
||||
content = (
|
||||
<Margins>
|
||||
<div className="py-6 flex flex-col gap-3">
|
||||
{task.workspace.latest_app_status && (
|
||||
<div className="p-3 border border-border border-solid rounded-lg">
|
||||
<WorkspaceAppStatus status={task.workspace.latest_app_status} />
|
||||
</div>
|
||||
)}
|
||||
<div className="border border-border border-solid rounded-lg w-full min-h-80 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="m-0 font-medium text-content-primary text-base">
|
||||
Task build terminated
|
||||
</h3>
|
||||
<span className="text-content-secondary text-sm">
|
||||
So apps and previous statuses are not available
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full min-h-80 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="m-0 font-medium text-content-primary text-base">
|
||||
Workspace is not running
|
||||
</h3>
|
||||
<span className="text-content-secondary text-sm">
|
||||
Apps and previous statuses are not available
|
||||
</span>
|
||||
<Button size="sm" className="mt-4" asChild>
|
||||
<RouterLink
|
||||
to={`/@${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
>
|
||||
View workspace
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Margins>
|
||||
@@ -180,53 +185,7 @@ const TaskPage = () => {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const statuses = task.workspace.latest_build.resources
|
||||
.flatMap((r) => r.agents)
|
||||
.flatMap((a) => a?.apps)
|
||||
.flatMap((a) => a?.statuses)
|
||||
.filter((s) => !!s)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
|
||||
content = (
|
||||
<div className="flex-1 flex justify-stretch overflow-hidden">
|
||||
<aside className="w-full max-w-xs border-0 border-r border-border border-solid">
|
||||
<ScrollArea className="h-full py-3">
|
||||
{statuses.map((status, index) => {
|
||||
return (
|
||||
<article
|
||||
className={cn(["px-4 py-3 flex gap-3"], {
|
||||
"opacity-75 hover:opacity-100": index !== 0,
|
||||
})}
|
||||
key={status.id}
|
||||
>
|
||||
<AppStatusIcon
|
||||
status={status}
|
||||
latest={index === 0}
|
||||
className="size-4 mt-1"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="m-0 font-medium text-sm">
|
||||
{status.message}
|
||||
</h3>
|
||||
<time
|
||||
dateTime={status.created_at}
|
||||
className="font-medium text-xs text-content-secondary"
|
||||
>
|
||||
{timeFrom(new Date(status.created_at))}
|
||||
</time>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
|
||||
<TaskApps task={task} />
|
||||
</div>
|
||||
);
|
||||
content = <TaskApps task={task} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -235,50 +194,151 @@ const TaskPage = () => {
|
||||
<title>{pageTitle(task.prompt)}</title>
|
||||
</Helmet>
|
||||
|
||||
<section className="h-full flex flex-col">
|
||||
<header className="h-20 border-0 border-b border-solid border-border px-4 flex items-center shrink-0 justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon-lg" variant="outline" asChild>
|
||||
<RouterLink to="/tasks">
|
||||
<ArrowLeftIcon />
|
||||
<span className="sr-only">Back to tasks</span>
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Back to tasks</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h1 className="m-0 text-sm font-medium">{task.prompt}</h1>
|
||||
<span className="text-xs text-content-secondary">
|
||||
Created by{" "}
|
||||
{task.workspace.owner_name ?? task.workspace.owner_name}{" "}
|
||||
{timeFrom(new Date(task.workspace.created_at))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" asChild>
|
||||
<RouterLink
|
||||
to={`/@${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
>
|
||||
View workspace
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="h-full flex justify-stretch">
|
||||
<TaskSidebar task={task} />
|
||||
{content}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskPage;
|
||||
|
||||
type TaskSidebarProps = {
|
||||
task: Task;
|
||||
};
|
||||
|
||||
const TaskSidebar: FC<TaskSidebarProps> = ({ task }) => {
|
||||
let statuses = task.workspace.latest_build.resources
|
||||
.flatMap((r) => r.agents)
|
||||
.flatMap((a) => a?.apps)
|
||||
.flatMap((a) => a?.statuses)
|
||||
.filter((s) => !!s)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
|
||||
// This happens when the workspace is not running so it has no resources to
|
||||
// get the statuses so we can fallback to the latest status received from the
|
||||
// workspace.
|
||||
if (statuses.length === 0 && task.workspace.latest_app_status) {
|
||||
statuses = [task.workspace.latest_app_status];
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="flex flex-col h-full border-0 border-r border-solid border-border w-[320px] shrink-0">
|
||||
<header className="border-0 border-b border-solid border-border p-4 pt-0">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="subtle" asChild className="-ml-2">
|
||||
<RouterLink to="/tasks">
|
||||
<ArrowLeftIcon />
|
||||
<span className="sr-only">Back to tasks</span>
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Back to tasks</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<DropdownMenu>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="subtle" className="-mr-2">
|
||||
<EllipsisVerticalIcon />
|
||||
<span className="sr-only">Settings</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<RouterLink
|
||||
to={`/@${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
>
|
||||
View workspace
|
||||
</RouterLink>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<h1 className="m-0 mt-1 text-base font-medium">{task.prompt}</h1>
|
||||
|
||||
{task.workspace.latest_app_status?.uri && (
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
<TaskStatusLink uri={task.workspace.latest_app_status.uri} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{statuses ? (
|
||||
<ScrollArea className="h-full">
|
||||
{statuses.length === 0 && (
|
||||
<article className="px-4 py-2 flex gap-2 first-of-type:pt-4 last-of-type:pb-4">
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<h3 className="m-0 font-medium text-sm leading-normal">
|
||||
Running your task
|
||||
</h3>
|
||||
<time
|
||||
dateTime={task.workspace.latest_build.created_at}
|
||||
className="font-medium text-xs text-content-secondary first-letter:uppercase"
|
||||
>
|
||||
{timeFrom(new Date(task.workspace.latest_build.created_at))}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<AppStatusStateIcon state="working" latest className="size-5" />
|
||||
</article>
|
||||
)}
|
||||
{statuses.map((status, index) => {
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
["px-4 py-2 flex gap-2 first-of-type:pt-4 last-of-type:pb-4"],
|
||||
{
|
||||
"opacity-50 hover:opacity-100": index !== 0,
|
||||
},
|
||||
)}
|
||||
key={status.id}
|
||||
>
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<h3 className="m-0 font-medium text-sm leading-normal">
|
||||
{status.message}
|
||||
</h3>
|
||||
<time
|
||||
dateTime={status.created_at}
|
||||
className="font-medium text-xs text-content-secondary first-letter:uppercase"
|
||||
>
|
||||
{timeFrom(new Date(status.created_at))}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<AppStatusStateIcon
|
||||
state={status.state}
|
||||
latest={index === 0}
|
||||
disabled={task.workspace.latest_build.status !== "running"}
|
||||
className={cn(["size-5", { "opacity-0": index !== 0 }])}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<Spinner loading />
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
type TaskAppsProps = {
|
||||
task: Task;
|
||||
};
|
||||
@@ -340,7 +400,7 @@ const TaskApps: FC<TaskAppsProps> = ({ task }) => {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="subtle">
|
||||
Open in IDE
|
||||
Open locally
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -465,6 +525,43 @@ const TaskAppIFrame: FC<TaskAppIFrameProps> = ({ task, app, active }) => {
|
||||
);
|
||||
};
|
||||
|
||||
type TaskStatusLinkProps = {
|
||||
uri: string;
|
||||
};
|
||||
|
||||
const TaskStatusLink: FC<TaskStatusLinkProps> = ({ uri }) => {
|
||||
let icon = <ExternalLinkIcon />;
|
||||
let label = truncateURI(uri);
|
||||
|
||||
if (uri.startsWith("https://github.com")) {
|
||||
const issueNumber = uri.split("/").pop();
|
||||
const [org, repo] = uri.split("/").slice(3, 5);
|
||||
const prefix = `${org}/${repo}`;
|
||||
|
||||
if (uri.includes("pull/")) {
|
||||
icon = <GitPullRequestArrowIcon />;
|
||||
label = issueNumber
|
||||
? `${prefix}#${issueNumber}`
|
||||
: `${prefix} Pull Request`;
|
||||
} else if (uri.includes("issues/")) {
|
||||
icon = <BugIcon />;
|
||||
label = issueNumber ? `${prefix}#${issueNumber}` : `${prefix} Issue`;
|
||||
} else {
|
||||
icon = <GitHub />;
|
||||
label = `${org}/${repo}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild variant="outline" size="sm" className="min-w-0">
|
||||
<a href={uri} target="_blank" rel="noreferrer">
|
||||
{icon}
|
||||
{label}
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const data = {
|
||||
fetchTask: async (workspaceOwnerUsername: string, workspaceName: string) => {
|
||||
const workspace = await API.getWorkspaceByOwnerAndName(
|
||||
|
||||
@@ -363,7 +363,10 @@ const TasksTable: FC<TasksTableProps> = ({ templates, filter }) => {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkspaceAppStatus status={workspace.latest_app_status} />
|
||||
<WorkspaceAppStatus
|
||||
disabled={workspace.latest_build.status !== "running"}
|
||||
status={workspace.latest_app_status}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
|
||||
@@ -21,44 +21,10 @@ import {
|
||||
FileIcon,
|
||||
LayoutGridIcon,
|
||||
} from "lucide-react";
|
||||
import { AppStatusIcon } from "modules/apps/AppStatusIcon";
|
||||
import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon";
|
||||
import { useAppLink } from "modules/apps/useAppLink";
|
||||
import { type FC, useState } from "react";
|
||||
|
||||
const formatURI = (uri: string) => {
|
||||
if (uri.startsWith("file://")) {
|
||||
const path = uri.slice(7);
|
||||
// Slightly shorter truncation for this context if needed
|
||||
if (path.length > 35) {
|
||||
const start = path.slice(0, 15);
|
||||
const end = path.slice(-15);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
const fullUrl = url.toString();
|
||||
// Slightly shorter truncation
|
||||
if (fullUrl.length > 40) {
|
||||
const start = fullUrl.slice(0, 20);
|
||||
const end = fullUrl.slice(-20);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return fullUrl;
|
||||
} catch {
|
||||
// Slightly shorter truncation
|
||||
if (uri.length > 35) {
|
||||
const start = uri.slice(0, 15);
|
||||
const end = uri.slice(-15);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Component Implementation ---
|
||||
import { truncateURI } from "utils/uri";
|
||||
|
||||
interface AppStatusesProps {
|
||||
workspace: Workspace;
|
||||
@@ -109,7 +75,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<div className="text-sm font-medium text-content-primary flex items-center gap-2 ">
|
||||
<AppStatusIcon status={latestStatus} latest />
|
||||
<AppStatusStateIcon state={latestStatus.state} latest />
|
||||
<span className="block flex-1 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{latestStatus.message}
|
||||
</span>
|
||||
@@ -135,7 +101,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
<TooltipTrigger>
|
||||
<span className="flex items-center gap-1">
|
||||
<FileIcon className="size-icon-xs" />
|
||||
{formatURI(latestStatus.uri)}
|
||||
{truncateURI(latestStatus.uri)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -147,7 +113,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a href={latestStatus.uri} target="_blank" rel="noreferrer">
|
||||
<ExternalLinkIcon />
|
||||
{formatURI(latestStatus.uri)}
|
||||
{truncateURI(latestStatus.uri)}
|
||||
</a>
|
||||
</Button>
|
||||
))}
|
||||
@@ -189,8 +155,8 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
>
|
||||
<div className="flex items-center justify-between w-full text-content-secondary">
|
||||
<span className="text-xs flex items-center gap-2">
|
||||
<AppStatusIcon
|
||||
status={latestStatus}
|
||||
<AppStatusStateIcon
|
||||
state={latestStatus.state}
|
||||
latest={false}
|
||||
className="size-icon-xs w-[18px]"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export const truncateURI = (uri: string) => {
|
||||
if (uri.startsWith("file://")) {
|
||||
const path = uri.slice(7);
|
||||
// Slightly shorter truncation for this context if needed
|
||||
if (path.length > 35) {
|
||||
const start = path.slice(0, 15);
|
||||
const end = path.slice(-15);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
const fullUrl = url.toString();
|
||||
// Slightly shorter truncation
|
||||
if (fullUrl.length > 30) {
|
||||
const start = fullUrl.slice(0, 15);
|
||||
const end = fullUrl.slice(-15);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return fullUrl;
|
||||
} catch {
|
||||
// Slightly shorter truncation
|
||||
if (uri.length > 20) {
|
||||
const start = uri.slice(0, 10);
|
||||
const end = uri.slice(-10);
|
||||
return `${start}...${end}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user