Skip to content

Test page environment selection #942

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 4 commits into from
Mar 14, 2024
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
12 changes: 6 additions & 6 deletions apps/webapp/app/components/code/InlineCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const variants = {
base: "text-base",
};

export type InlineCodeVariant = keyof typeof variants;

type InlineCodeProps = {
children: React.ReactNode;
variant?: InlineCodeVariant;
className?: string;
};

type VariantProps = InlineCodeProps & {
variant?: keyof typeof variants;
};

export function InlineCode({ variant = "small", children }: VariantProps) {
return <code className={cn(inlineCode, variants[variant])}>{children}</code>;
export function InlineCode({ variant = "small", children, className }: InlineCodeProps) {
return <code className={cn(inlineCode, variants[variant], className)}>{children}</code>;
}
6 changes: 4 additions & 2 deletions apps/webapp/app/components/primitives/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type TableCellProps = TableCellBasicProps & {
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
hasAction?: boolean;
isSticky?: boolean;
actionClassName?: string;
};

const stickyStyles =
Expand All @@ -132,6 +133,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
(
{
className,
actionClassName,
alignment = "left",
children,
colSpan,
Expand Down Expand Up @@ -176,11 +178,11 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
colSpan={colSpan}
>
{to ? (
<Link to={to} className={flexClasses}>
<Link to={to} className={cn(flexClasses, actionClassName)}>
{children}
</Link>
) : onClick ? (
<button onClick={onClick} className={flexClasses}>
<button onClick={onClick} className={cn(flexClasses, actionClassName)}>
{children}
</button>
) : (
Expand Down
16 changes: 16 additions & 0 deletions apps/webapp/app/components/runs/v3/TaskPath.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { InlineCode, InlineCodeVariant } from "~/components/code/InlineCode";
import { SpanCodePathAccessory } from "./SpanTitle";
import { cn } from "~/utils/cn";

type TaskPathProps = {
filePath: string;
Expand All @@ -16,3 +18,17 @@ export function TaskPath({ filePath, functionName, className }: TaskPathProps) {
/>
);
}

type TaskFunctionNameProps = {
functionName: string;
variant?: InlineCodeVariant;
className?: string;
};

export function TaskFunctionName({ variant, functionName, className }: TaskFunctionNameProps) {
return (
<InlineCode variant={variant} className={cn("text-sun-100", className)}>
{functionName}()
</InlineCode>
);
}
66 changes: 66 additions & 0 deletions apps/webapp/app/hooks/useLinkStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useLocation, useNavigation, useResolvedPath } from "@remix-run/react";
import type { RelativeRoutingType } from "@remix-run/router";

//A lot of this logic is lifted from <NavLink> in react-router-dom, thanks again Remix team ❤️.
//https://github.com/remix-run/react-router/blob/a04ae6b90127ae583be08432c52b951e53f6a3c7/packages/react-router-dom/index.tsx#L1010

type Options = {
/** Defines the relative path behavior for the link.
*
* route - default, relative to the route hierarchy so .. will remove all URL segments of the current route pattern
*
* path - relative to the path so .. will remove one URL segment
*/
relative?: RelativeRoutingType;
/** 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. */
end?: boolean;
};

type Result = {
isActive: boolean;
isPending: boolean;
isTransitioning: boolean;
};

/** 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 */
export function useLinkStatus(to: string, options?: Options): Result {
const { relative, end = false } = options || {};

const path = useResolvedPath(to, { relative: relative });
const pathName = path.pathname.toLowerCase();

//current location and pending location (if there is one)
const location = useLocation();
const locationPathname = location.pathname.toLowerCase();
const navigation = useNavigation();
const nextLocationPathname = navigation.location
? navigation.location.pathname.toLowerCase()
: null;

// If the `to` has a trailing slash, look at that exact spot. Otherwise,
// we're looking for a slash _after_ what's in `to`. For example:
//
// <NavLink to="/users"> and <NavLink to="/users/">
// both want to look for a / at index 6 to match URL `/users/matt`
const endSlashPosition =
pathName !== "/" && pathName.endsWith("/") ? pathName.length - 1 : pathName.length;

const isActive =
locationPathname === pathName ||
(!end &&
locationPathname.startsWith(pathName) &&
locationPathname.charAt(endSlashPosition) === "/");

const isPending =
nextLocationPathname != null &&
(nextLocationPathname === pathName ||
(!end &&
nextLocationPathname.startsWith(pathName) &&
nextLocationPathname.charAt(pathName.length) === "/"));

return {
isActive,
isPending,
isTransitioning: navigation.state === "loading",
};
}
87 changes: 55 additions & 32 deletions apps/webapp/app/presenters/v3/TestPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { PrismaClient, prisma } from "~/db.server";
import { TestSearchParams } from "~/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.test/route";
import { sortEnvironments } from "~/services/environmentSort.server";
import { createSearchParams } from "~/utils/searchParams";
import { getUsername } from "~/utils/username";

type TaskListOptions = {
userId: string;
projectSlug: string;
url: string;
};

export type TaskList = Awaited<ReturnType<TestPresenter["call"]>>;
export type TaskListItem = TaskList["tasks"][0];
export type TaskListItem = NonNullable<TaskList["tasks"]>[0];
export type SelectedEnvironment = NonNullable<TaskList["selectedEnvironment"]>;

export class TestPresenter {
#prismaClient: PrismaClient;
Expand All @@ -16,7 +21,7 @@ export class TestPresenter {
this.#prismaClient = prismaClient;
}

public async call({ userId, projectSlug }: TaskListOptions) {
public async call({ userId, projectSlug, url }: TaskListOptions) {
// Find the project scoped to the organization
const project = await this.#prismaClient.project.findFirstOrThrow({
select: {
Expand All @@ -26,17 +31,18 @@ export class TestPresenter {
id: true,
type: true,
slug: true,
orgMember: {
select: {
user: {
select: {
id: true,
name: true,
displayName: true,
},
},
where: {
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
},
],
},
},
},
Expand All @@ -45,55 +51,72 @@ export class TestPresenter {
},
});

const environments = sortEnvironments(
project.environments.map((environment) => ({
id: environment.id,
type: environment.type,
slug: environment.slug,
}))
);

const searchParams = createSearchParams(url, TestSearchParams);

//no environmentId
if (!searchParams.success || !searchParams.params.get("environment")) {
return {
hasSelectedEnvironment: false as const,
environments,
};
}

//is the environmentId valid?
const matchingEnvironment = project.environments.find(
(env) => env.slug === searchParams.params.get("environment")
);
if (!matchingEnvironment) {
return {
hasSelectedEnvironment: false as const,
environments,
};
}

//get all possible tasks
const tasks = await this.#prismaClient.$queryRaw<
{
id: string;
version: string;
runtimeEnvironmentId: string;
taskIdentifier: string;
filePath: string;
exportName: string;
friendlyId: string;
}[]
>`
WITH workers AS (
>`WITH workers AS (
SELECT
bw.*,
ROW_NUMBER() OVER(PARTITION BY bw."runtimeEnvironmentId" ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
FROM
"BackgroundWorker" bw
WHERE "projectId" = ${project.id}
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
),
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
SELECT "BackgroundWorkerTask".id, version, "BackgroundWorkerTask"."runtimeEnvironmentId", slug as "taskIdentifier", "filePath", "exportName", "BackgroundWorkerTask"."friendlyId"
SELECT "BackgroundWorkerTask".id, version, slug as "taskIdentifier", "filePath", "exportName", "BackgroundWorkerTask"."friendlyId"
FROM latest_workers
JOIN "BackgroundWorkerTask" ON "BackgroundWorkerTask"."workerId" = latest_workers.id;;
JOIN "BackgroundWorkerTask" ON "BackgroundWorkerTask"."workerId" = latest_workers.id
ORDER BY "BackgroundWorkerTask"."exportName" ASC;
`;

return {
hasSelectedEnvironment: true as const,
environments,
selectedEnvironment: matchingEnvironment,
tasks: tasks.map((task) => {
const environment = project.environments.find(
(env) => env.id === task.runtimeEnvironmentId
);

if (!environment) {
throw new Error(`Environment not found for Task ${task.id}`);
}

return {
id: task.id,
version: task.version,
taskIdentifier: task.taskIdentifier,
filePath: task.filePath,
exportName: task.exportName,
friendlyId: task.friendlyId,
environment: {
type: environment.type,
slug: environment.slug,
userId: environment.orgMember?.user.id,
userName: getUsername(environment.orgMember?.user),
},
};
}),
};
Expand Down
Loading