Skip to content

Commit 819b663

Browse files
authored
Test page environment selection (#942)
* useLinkStatus hook * Now you can customize the appearance of InlineCode * Created <TaskFunctionName/> * The test page now has an environment selector at the top
1 parent 478ce00 commit 819b663

File tree

10 files changed

+382
-104
lines changed

10 files changed

+382
-104
lines changed

apps/webapp/app/components/code/InlineCode.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ const variants = {
1010
base: "text-base",
1111
};
1212

13+
export type InlineCodeVariant = keyof typeof variants;
14+
1315
type InlineCodeProps = {
1416
children: React.ReactNode;
17+
variant?: InlineCodeVariant;
18+
className?: string;
1519
};
1620

17-
type VariantProps = InlineCodeProps & {
18-
variant?: keyof typeof variants;
19-
};
20-
21-
export function InlineCode({ variant = "small", children }: VariantProps) {
22-
return <code className={cn(inlineCode, variants[variant])}>{children}</code>;
21+
export function InlineCode({ variant = "small", children, className }: InlineCodeProps) {
22+
return <code className={cn(inlineCode, variants[variant], className)}>{children}</code>;
2323
}

apps/webapp/app/components/primitives/Table.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type TableCellProps = TableCellBasicProps & {
123123
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
124124
hasAction?: boolean;
125125
isSticky?: boolean;
126+
actionClassName?: string;
126127
};
127128

128129
const stickyStyles =
@@ -132,6 +133,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
132133
(
133134
{
134135
className,
136+
actionClassName,
135137
alignment = "left",
136138
children,
137139
colSpan,
@@ -176,11 +178,11 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
176178
colSpan={colSpan}
177179
>
178180
{to ? (
179-
<Link to={to} className={flexClasses}>
181+
<Link to={to} className={cn(flexClasses, actionClassName)}>
180182
{children}
181183
</Link>
182184
) : onClick ? (
183-
<button onClick={onClick} className={flexClasses}>
185+
<button onClick={onClick} className={cn(flexClasses, actionClassName)}>
184186
{children}
185187
</button>
186188
) : (

apps/webapp/app/components/runs/v3/TaskPath.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { InlineCode, InlineCodeVariant } from "~/components/code/InlineCode";
12
import { SpanCodePathAccessory } from "./SpanTitle";
3+
import { cn } from "~/utils/cn";
24

35
type TaskPathProps = {
46
filePath: string;
@@ -16,3 +18,17 @@ export function TaskPath({ filePath, functionName, className }: TaskPathProps) {
1618
/>
1719
);
1820
}
21+
22+
type TaskFunctionNameProps = {
23+
functionName: string;
24+
variant?: InlineCodeVariant;
25+
className?: string;
26+
};
27+
28+
export function TaskFunctionName({ variant, functionName, className }: TaskFunctionNameProps) {
29+
return (
30+
<InlineCode variant={variant} className={cn("text-sun-100", className)}>
31+
{functionName}()
32+
</InlineCode>
33+
);
34+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useLocation, useNavigation, useResolvedPath } from "@remix-run/react";
2+
import type { RelativeRoutingType } from "@remix-run/router";
3+
4+
//A lot of this logic is lifted from <NavLink> in react-router-dom, thanks again Remix team ❤️.
5+
//https://github.com/remix-run/react-router/blob/a04ae6b90127ae583be08432c52b951e53f6a3c7/packages/react-router-dom/index.tsx#L1010
6+
7+
type Options = {
8+
/** Defines the relative path behavior for the link.
9+
*
10+
* route - default, relative to the route hierarchy so .. will remove all URL segments of the current route pattern
11+
*
12+
* path - relative to the path so .. will remove one URL segment
13+
*/
14+
relative?: RelativeRoutingType;
15+
/** The end prop changes the matching logic for the active and pending states to only match to the "end" of the NavLinks's to path. If the URL is longer than to, it will no longer be considered active. */
16+
end?: boolean;
17+
};
18+
19+
type Result = {
20+
isActive: boolean;
21+
isPending: boolean;
22+
isTransitioning: boolean;
23+
};
24+
25+
/** Pass a relative link and you will get back whether it's the current page, about to be and whether the route is currently changing */
26+
export function useLinkStatus(to: string, options?: Options): Result {
27+
const { relative, end = false } = options || {};
28+
29+
const path = useResolvedPath(to, { relative: relative });
30+
const pathName = path.pathname.toLowerCase();
31+
32+
//current location and pending location (if there is one)
33+
const location = useLocation();
34+
const locationPathname = location.pathname.toLowerCase();
35+
const navigation = useNavigation();
36+
const nextLocationPathname = navigation.location
37+
? navigation.location.pathname.toLowerCase()
38+
: null;
39+
40+
// If the `to` has a trailing slash, look at that exact spot. Otherwise,
41+
// we're looking for a slash _after_ what's in `to`. For example:
42+
//
43+
// <NavLink to="/users"> and <NavLink to="/users/">
44+
// both want to look for a / at index 6 to match URL `/users/matt`
45+
const endSlashPosition =
46+
pathName !== "/" && pathName.endsWith("/") ? pathName.length - 1 : pathName.length;
47+
48+
const isActive =
49+
locationPathname === pathName ||
50+
(!end &&
51+
locationPathname.startsWith(pathName) &&
52+
locationPathname.charAt(endSlashPosition) === "/");
53+
54+
const isPending =
55+
nextLocationPathname != null &&
56+
(nextLocationPathname === pathName ||
57+
(!end &&
58+
nextLocationPathname.startsWith(pathName) &&
59+
nextLocationPathname.charAt(pathName.length) === "/"));
60+
61+
return {
62+
isActive,
63+
isPending,
64+
isTransitioning: navigation.state === "loading",
65+
};
66+
}

apps/webapp/app/presenters/v3/TestPresenter.server.ts

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { PrismaClient, prisma } from "~/db.server";
2+
import { TestSearchParams } from "~/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.test/route";
3+
import { sortEnvironments } from "~/services/environmentSort.server";
4+
import { createSearchParams } from "~/utils/searchParams";
25
import { getUsername } from "~/utils/username";
36

47
type TaskListOptions = {
58
userId: string;
69
projectSlug: string;
10+
url: string;
711
};
812

913
export type TaskList = Awaited<ReturnType<TestPresenter["call"]>>;
10-
export type TaskListItem = TaskList["tasks"][0];
14+
export type TaskListItem = NonNullable<TaskList["tasks"]>[0];
15+
export type SelectedEnvironment = NonNullable<TaskList["selectedEnvironment"]>;
1116

1217
export class TestPresenter {
1318
#prismaClient: PrismaClient;
@@ -16,7 +21,7 @@ export class TestPresenter {
1621
this.#prismaClient = prismaClient;
1722
}
1823

19-
public async call({ userId, projectSlug }: TaskListOptions) {
24+
public async call({ userId, projectSlug, url }: TaskListOptions) {
2025
// Find the project scoped to the organization
2126
const project = await this.#prismaClient.project.findFirstOrThrow({
2227
select: {
@@ -26,17 +31,18 @@ export class TestPresenter {
2631
id: true,
2732
type: true,
2833
slug: true,
29-
orgMember: {
30-
select: {
31-
user: {
32-
select: {
33-
id: true,
34-
name: true,
35-
displayName: true,
36-
},
34+
},
35+
where: {
36+
OR: [
37+
{
38+
orgMember: null,
39+
},
40+
{
41+
orgMember: {
42+
userId,
3743
},
3844
},
39-
},
45+
],
4046
},
4147
},
4248
},
@@ -45,55 +51,72 @@ export class TestPresenter {
4551
},
4652
});
4753

54+
const environments = sortEnvironments(
55+
project.environments.map((environment) => ({
56+
id: environment.id,
57+
type: environment.type,
58+
slug: environment.slug,
59+
}))
60+
);
61+
62+
const searchParams = createSearchParams(url, TestSearchParams);
63+
64+
//no environmentId
65+
if (!searchParams.success || !searchParams.params.get("environment")) {
66+
return {
67+
hasSelectedEnvironment: false as const,
68+
environments,
69+
};
70+
}
71+
72+
//is the environmentId valid?
73+
const matchingEnvironment = project.environments.find(
74+
(env) => env.slug === searchParams.params.get("environment")
75+
);
76+
if (!matchingEnvironment) {
77+
return {
78+
hasSelectedEnvironment: false as const,
79+
environments,
80+
};
81+
}
82+
4883
//get all possible tasks
4984
const tasks = await this.#prismaClient.$queryRaw<
5085
{
5186
id: string;
5287
version: string;
53-
runtimeEnvironmentId: string;
5488
taskIdentifier: string;
5589
filePath: string;
5690
exportName: string;
5791
friendlyId: string;
5892
}[]
59-
>`
60-
WITH workers AS (
93+
>`WITH workers AS (
6194
SELECT
6295
bw.*,
63-
ROW_NUMBER() OVER(PARTITION BY bw."runtimeEnvironmentId" ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
96+
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
6497
FROM
6598
"BackgroundWorker" bw
66-
WHERE "projectId" = ${project.id}
99+
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
67100
),
68101
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
69-
SELECT "BackgroundWorkerTask".id, version, "BackgroundWorkerTask"."runtimeEnvironmentId", slug as "taskIdentifier", "filePath", "exportName", "BackgroundWorkerTask"."friendlyId"
102+
SELECT "BackgroundWorkerTask".id, version, slug as "taskIdentifier", "filePath", "exportName", "BackgroundWorkerTask"."friendlyId"
70103
FROM latest_workers
71-
JOIN "BackgroundWorkerTask" ON "BackgroundWorkerTask"."workerId" = latest_workers.id;;
104+
JOIN "BackgroundWorkerTask" ON "BackgroundWorkerTask"."workerId" = latest_workers.id
105+
ORDER BY "BackgroundWorkerTask"."exportName" ASC;
72106
`;
73107

74108
return {
109+
hasSelectedEnvironment: true as const,
110+
environments,
111+
selectedEnvironment: matchingEnvironment,
75112
tasks: tasks.map((task) => {
76-
const environment = project.environments.find(
77-
(env) => env.id === task.runtimeEnvironmentId
78-
);
79-
80-
if (!environment) {
81-
throw new Error(`Environment not found for Task ${task.id}`);
82-
}
83-
84113
return {
85114
id: task.id,
86115
version: task.version,
87116
taskIdentifier: task.taskIdentifier,
88117
filePath: task.filePath,
89118
exportName: task.exportName,
90119
friendlyId: task.friendlyId,
91-
environment: {
92-
type: environment.type,
93-
slug: environment.slug,
94-
userId: environment.orgMember?.user.id,
95-
userName: getUsername(environment.orgMember?.user),
96-
},
97120
};
98121
}),
99122
};

0 commit comments

Comments
 (0)