Merge branch 'main' into v3/fix-consecutive-waits
This commit is contained in:
@@ -17,4 +17,5 @@ build-storybook.log
|
||||
.storybook-out
|
||||
storybook-static
|
||||
|
||||
/prisma/seed.js
|
||||
/prisma/seed.js
|
||||
/prisma/populate.js
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { VirtualElement as IVirtualElement } from "@popperjs/core";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
import { useEvent } from "react-use";
|
||||
import useLazyRef from "~/hooks/useLazyRef";
|
||||
|
||||
// Recharts 3.x will have portal support, but until then we're using this:
|
||||
//https://github.com/recharts/recharts/issues/2458#issuecomment-1063463873
|
||||
|
||||
export interface PopperPortalProps {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function TooltipPortal({ active = true, children }: PopperPortalProps) {
|
||||
const [portalElement, setPortalElement] = useState<HTMLDivElement>();
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
|
||||
const virtualElementRef = useLazyRef(() => new VirtualElement());
|
||||
|
||||
const { styles, attributes, update } = usePopper(
|
||||
virtualElementRef.current,
|
||||
popperElement,
|
||||
POPPER_OPTIONS
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
setPortalElement(el);
|
||||
return () => el.remove();
|
||||
}, []);
|
||||
|
||||
useEvent("mousemove", ({ clientX: x, clientY: y }) => {
|
||||
virtualElementRef.current?.update(x, y);
|
||||
if (!active) return;
|
||||
update?.();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
update?.();
|
||||
}, [active, update]);
|
||||
|
||||
if (!portalElement) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
{...attributes.popper}
|
||||
style={{
|
||||
...styles.popper,
|
||||
zIndex: 1000,
|
||||
display: active ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
portalElement
|
||||
);
|
||||
}
|
||||
|
||||
class VirtualElement implements IVirtualElement {
|
||||
private rect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
update(x: number, y: number) {
|
||||
this.rect.y = y;
|
||||
this.rect.top = y;
|
||||
this.rect.bottom = y;
|
||||
|
||||
this.rect.x = x;
|
||||
this.rect.left = x;
|
||||
this.rect.right = x;
|
||||
}
|
||||
|
||||
getBoundingClientRect(): DOMRect {
|
||||
return this.rect;
|
||||
}
|
||||
}
|
||||
|
||||
const POPPER_OPTIONS: Parameters<typeof usePopper>[2] = {
|
||||
placement: "right-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [8, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,18 +1,14 @@
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function LiveTimer({
|
||||
startTime,
|
||||
endTime,
|
||||
updateInterval = 250,
|
||||
className,
|
||||
}: {
|
||||
startTime: Date;
|
||||
endTime?: Date;
|
||||
updateInterval?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const [now, setNow] = useState<Date>();
|
||||
|
||||
@@ -30,13 +26,13 @@ export function LiveTimer({
|
||||
}, [startTime]);
|
||||
|
||||
return (
|
||||
<Paragraph variant="extra-small" className={cn("whitespace-nowrap tabular-nums", className)}>
|
||||
<>
|
||||
{formatDuration(startTime, now, {
|
||||
style: "short",
|
||||
maxDecimalPoints: 0,
|
||||
units: ["d", "h", "m", "s"],
|
||||
})}
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@ export const RUNNING_STATUSES: TaskRunStatus[] = [
|
||||
"WAITING_TO_RESUME",
|
||||
];
|
||||
|
||||
export const FINISHED_STATUSES: TaskRunStatus[] = [
|
||||
"COMPLETED_SUCCESSFULLY",
|
||||
"CANCELED",
|
||||
"COMPLETED_WITH_ERRORS",
|
||||
"INTERRUPTED",
|
||||
"SYSTEM_FAILURE",
|
||||
"CRASHED",
|
||||
];
|
||||
|
||||
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
|
||||
return taskRunStatusDescriptions[status];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
import { LiveTimer } from "./LiveTimer";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
@@ -94,9 +94,15 @@ export function TaskRunsTable({
|
||||
{run.startedAt ? <DateTime date={run.startedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{formatDuration(run.startedAt, run.completedAt, {
|
||||
style: "short",
|
||||
})}
|
||||
{run.startedAt && run.finishedAt ? (
|
||||
formatDuration(new Date(run.startedAt), new Date(run.finishedAt), {
|
||||
style: "short",
|
||||
})
|
||||
) : run.startedAt ? (
|
||||
<LiveTimer startTime={new Date(run.startedAt)} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{run.isTest ? (
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useRef, MutableRefObject } from "react";
|
||||
|
||||
const useLazyRef = <T>(initialValFunc: () => T) => {
|
||||
const ref: MutableRefObject<T | null> = useRef(null);
|
||||
if (ref.current === null) {
|
||||
ref.current = initialValFunc();
|
||||
}
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useLazyRef;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
type RunListOptions = {
|
||||
userId?: string;
|
||||
@@ -28,13 +29,7 @@ export type RunList = Awaited<ReturnType<RunListPresenter["call"]>>;
|
||||
export type RunListItem = RunList["runs"][0];
|
||||
export type RunListAppliedFilters = RunList["filters"];
|
||||
|
||||
export class RunListPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
export class RunListPresenter extends BasePresenter {
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
@@ -60,7 +55,7 @@ export class RunListPresenter {
|
||||
to !== undefined;
|
||||
|
||||
// Find the project scoped to the organization
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
const project = await this._replica.project.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
@@ -88,7 +83,7 @@ export class RunListPresenter {
|
||||
});
|
||||
|
||||
//get all possible tasks
|
||||
const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({
|
||||
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
|
||||
distinct: ["slug"],
|
||||
where: {
|
||||
projectId: project.id,
|
||||
@@ -96,7 +91,7 @@ export class RunListPresenter {
|
||||
});
|
||||
|
||||
//get the runs
|
||||
let runs = await this.#prismaClient.$queryRaw<
|
||||
let runs = await this._replica.$queryRaw<
|
||||
{
|
||||
id: string;
|
||||
number: BigInt;
|
||||
@@ -107,10 +102,9 @@ export class RunListPresenter {
|
||||
status: TaskRunStatus;
|
||||
createdAt: Date;
|
||||
lockedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
updatedAt: Date;
|
||||
isTest: boolean;
|
||||
spanId: string;
|
||||
attempts: BigInt;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
@@ -123,20 +117,13 @@ export class RunListPresenter {
|
||||
tr.status AS status,
|
||||
tr."createdAt" AS "createdAt",
|
||||
tr."lockedAt" AS "lockedAt",
|
||||
tra."completedAt" AS "completedAt",
|
||||
tr."updatedAt" AS "updatedAt",
|
||||
tr."isTest" AS "isTest",
|
||||
tr."spanId" AS "spanId",
|
||||
COUNT(tra.id) AS attempts
|
||||
tr."spanId" AS "spanId"
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
LEFT JOIN
|
||||
(
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (PARTITION BY "taskRunId" ORDER BY "createdAt" DESC) rn
|
||||
FROM ${sqlDatabaseSchema}."TaskRunAttempt"
|
||||
) tra ON tr.id = tra."taskRunId" AND tra.rn = 1
|
||||
LEFT JOIN
|
||||
${sqlDatabaseSchema}."BackgroundWorker" bw ON tra."backgroundWorkerId" = bw.id
|
||||
${sqlDatabaseSchema}."BackgroundWorker" bw ON tr."lockedToVersionId" = bw.id
|
||||
WHERE
|
||||
-- project
|
||||
tr."projectId" = ${project.id}
|
||||
@@ -154,15 +141,11 @@ export class RunListPresenter {
|
||||
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${hasStatusFilters ? Prisma.sql`AND (` : Prisma.empty}
|
||||
${
|
||||
statuses && statuses.length > 0
|
||||
? Prisma.sql`tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
||||
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
||||
: Prisma.empty
|
||||
}
|
||||
${statuses && statuses.length > 0 && hasStatusFilters ? Prisma.sql` OR ` : Prisma.empty}
|
||||
${hasStatusFilters ? Prisma.sql`tr.status IS NULL` : Prisma.empty}
|
||||
${hasStatusFilters ? Prisma.sql`) ` : Prisma.empty}
|
||||
${
|
||||
environments && environments.length > 0
|
||||
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
|
||||
@@ -179,8 +162,6 @@ export class RunListPresenter {
|
||||
? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp`
|
||||
: Prisma.empty
|
||||
}
|
||||
GROUP BY
|
||||
tr."friendlyId", tr."taskIdentifier", tr."runtimeEnvironmentId", tr.id, bw.version, tra.status, tr."createdAt", tra."startedAt", tra."completedAt"
|
||||
ORDER BY
|
||||
${direction === "forward" ? Prisma.sql`tr.id DESC` : Prisma.sql`tr.id ASC`}
|
||||
LIMIT ${pageSize + 1}`;
|
||||
@@ -219,19 +200,21 @@ export class RunListPresenter {
|
||||
throw new Error(`Environment not found for TaskRun ${run.id}`);
|
||||
}
|
||||
|
||||
const hasFinished = FINISHED_STATUSES.includes(run.status);
|
||||
|
||||
return {
|
||||
id: run.id,
|
||||
friendlyId: run.runFriendlyId,
|
||||
number: Number(run.number),
|
||||
createdAt: run.createdAt,
|
||||
startedAt: run.lockedAt,
|
||||
completedAt: run.completedAt,
|
||||
createdAt: run.createdAt.toISOString(),
|
||||
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
|
||||
hasFinished,
|
||||
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
|
||||
isTest: run.isTest,
|
||||
status: run.status,
|
||||
version: run.version,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
spanId: run.spanId,
|
||||
attempts: Number(run.attempts),
|
||||
isReplayable: true,
|
||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||
environment: displayableEnvironments(environment, userId),
|
||||
|
||||
@@ -4,16 +4,15 @@ import {
|
||||
TaskRunStatus,
|
||||
TaskTriggerSource,
|
||||
} from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
|
||||
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { sortEnvironments } from "~/services/environmentSort.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
|
||||
export type Task = {
|
||||
slug: string;
|
||||
@@ -26,10 +25,6 @@ export type Task = {
|
||||
type: RuntimeEnvironmentType;
|
||||
userName?: string;
|
||||
}[];
|
||||
latestRun?: {
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
};
|
||||
};
|
||||
|
||||
type Return = Awaited<ReturnType<TaskListPresenter["call"]>>;
|
||||
@@ -98,39 +93,8 @@ export class TaskListPresenter extends BasePresenter {
|
||||
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" tasks ON tasks."workerId" = workers.id
|
||||
ORDER BY slug ASC;`;
|
||||
|
||||
let latestRuns = [] as {
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
taskIdentifier: string;
|
||||
}[];
|
||||
|
||||
if (tasks.length > 0) {
|
||||
const uniqueTaskSlugs = new Set(tasks.map((t) => t.slug));
|
||||
latestRuns = await this._replica.$queryRaw<
|
||||
{
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
taskIdentifier: string;
|
||||
}[]
|
||||
>`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
"createdAt",
|
||||
"status",
|
||||
"taskIdentifier",
|
||||
ROW_NUMBER() OVER (PARTITION BY "taskIdentifier" ORDER BY "updatedAt" DESC) AS rn
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun"
|
||||
WHERE
|
||||
"taskIdentifier" IN(${Prisma.join(Array.from(uniqueTaskSlugs))})
|
||||
AND "projectId" = ${project.id}
|
||||
) t
|
||||
WHERE rn = 1;`;
|
||||
}
|
||||
|
||||
//group by the task identifier (task.slug). Add the latestRun and add all the environments.
|
||||
const outputTasks = tasks.reduce((acc, task) => {
|
||||
const latestRun = latestRuns.find((r) => r.taskIdentifier === task.slug);
|
||||
const environment = project.environments.find((env) => env.id === task.runtimeEnvironmentId);
|
||||
if (!environment) {
|
||||
throw new Error(`Environment not found for TaskRun ${task.id}`);
|
||||
@@ -151,13 +115,6 @@ export class TaskListPresenter extends BasePresenter {
|
||||
//order the environments
|
||||
existingTask.environments = sortEnvironments(existingTask.environments);
|
||||
|
||||
existingTask.latestRun = latestRun
|
||||
? {
|
||||
createdAt: latestRun.createdAt,
|
||||
status: latestRun.status,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return acc;
|
||||
}, [] as Task[]);
|
||||
|
||||
@@ -186,6 +143,10 @@ export class TaskListPresenter extends BasePresenter {
|
||||
}
|
||||
|
||||
async #getActivity(tasks: string[], projectId: string) {
|
||||
if (tasks.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const activity = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
@@ -257,6 +218,10 @@ export class TaskListPresenter extends BasePresenter {
|
||||
}
|
||||
|
||||
async #getRunningStats(tasks: string[], projectId: string) {
|
||||
if (tasks.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const statuses = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
@@ -305,6 +270,10 @@ export class TaskListPresenter extends BasePresenter {
|
||||
}
|
||||
|
||||
async #getAverageDurations(tasks: string[], projectId: string) {
|
||||
if (tasks.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const durations = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
|
||||
+73
-54
@@ -1,10 +1,10 @@
|
||||
import { ChatBubbleLeftRightIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Fragment, Suspense, useEffect, useState } from "react";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
|
||||
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
|
||||
@@ -14,8 +14,9 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime, formatDateTime } from "~/components/primitives/DateTime";
|
||||
import { formatDateTime } from "~/components/primitives/DateTime";
|
||||
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
@@ -31,13 +32,9 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import TooltipPortal from "~/components/primitives/TooltipPortal";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import {
|
||||
TaskRunStatusCombo,
|
||||
TaskRunStatusIcon,
|
||||
runStatusClassNameColor,
|
||||
runStatusTitle,
|
||||
} from "~/components/runs/v3/TaskRunStatus";
|
||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||
import {
|
||||
TaskTriggerSourceIcon,
|
||||
taskTriggerSourceDescription,
|
||||
@@ -45,8 +42,8 @@ import {
|
||||
import { useEventSource } from "~/hooks/useEventSource";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { Task, TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3RunsPath, v3TasksStreamingPath } from "~/utils/pathBuilder";
|
||||
@@ -84,6 +81,31 @@ export default function Page() {
|
||||
const project = useProject();
|
||||
const { tasks, userHasTasks, activity, runningStats, durations } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Task>({
|
||||
items: tasks,
|
||||
filter: (task, text) => {
|
||||
if (task.slug.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
task.exportName.toLowerCase().includes(text.toLowerCase().replace("(", "").replace(")", ""))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const hasTasks = tasks.length > 0;
|
||||
|
||||
//live reload the page when the tasks change
|
||||
@@ -105,11 +127,22 @@ export default function Page() {
|
||||
<PageTitle title="Tasks" />
|
||||
</NavBar>
|
||||
<PageBody>
|
||||
<div className={cn("grid h-full grid-cols-1 gap-4")}>
|
||||
<div className="h-full">
|
||||
{hasTasks ? (
|
||||
<div className="flex flex-col gap-4 pb-4">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<div className={cn("grid h-full grid-rows-1")}>
|
||||
{hasTasks ? (
|
||||
<div className="flex flex-col gap-4 pb-4">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<div className="pb-4">
|
||||
<div className="h-8">
|
||||
<Input
|
||||
placeholder="Search tasks"
|
||||
variant="tertiary"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -120,13 +153,12 @@ export default function Page() {
|
||||
<TableHeaderCell>Activity (7d)</TableHeaderCell>
|
||||
<TableHeaderCell>Avg. duration</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.length > 0 ? (
|
||||
tasks.map((task) => {
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map((task) => {
|
||||
const path = v3RunsPath(organization, project, {
|
||||
tasks: [task.slug],
|
||||
});
|
||||
@@ -218,30 +250,12 @@ export default function Page() {
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{task.latestRun ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1",
|
||||
runStatusClassNameColor(task.latestRun.status)
|
||||
)}
|
||||
>
|
||||
<TaskRunStatusIcon
|
||||
status={task.latestRun.status}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<DateTime date={task.latestRun.createdAt} />
|
||||
</div>
|
||||
) : (
|
||||
"Never run"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={6}>
|
||||
<TableBlankRow colSpan={8}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
No tasks match your filters
|
||||
</Paragraph>
|
||||
@@ -250,12 +264,12 @@ export default function Page() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<CreateTaskInstructions />
|
||||
</MainCenteredContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<CreateTaskInstructions />
|
||||
</MainCenteredContainer>
|
||||
)}
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
@@ -362,7 +376,9 @@ function TaskActivityGraph({ activity }: { activity: TaskActivity }) {
|
||||
content={<CustomTooltip />}
|
||||
allowEscapeViewBox={{ x: true, y: true }}
|
||||
wrapperStyle={{ zIndex: 1000 }}
|
||||
animationDuration={0}
|
||||
/>
|
||||
|
||||
{/* The background */}
|
||||
<Bar
|
||||
dataKey="bg"
|
||||
@@ -425,18 +441,21 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>)
|
||||
}));
|
||||
const title = payload[0].payload.day as string;
|
||||
const formattedDate = formatDateTime(new Date(title), "UTC", [], false, false);
|
||||
|
||||
return (
|
||||
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
|
||||
<Header3 className="border-b-charcoal-650 border-b pb-2">{formattedDate}</Header3>
|
||||
<div className="mt-2 grid grid-cols-[1fr_auto] gap-2 text-xs text-text-bright">
|
||||
{items.map((item) => (
|
||||
<Fragment key={item.status}>
|
||||
<TaskRunStatusCombo status={item.status} />
|
||||
<p>{item.value}</p>
|
||||
</Fragment>
|
||||
))}
|
||||
<TooltipPortal active={active}>
|
||||
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
|
||||
<Header3 className="border-b-charcoal-650 border-b pb-2">{formattedDate}</Header3>
|
||||
<div className="mt-2 grid grid-cols-[1fr_auto] gap-2 text-xs text-text-bright">
|
||||
{items.map((item) => (
|
||||
<Fragment key={item.status}>
|
||||
<TaskRunStatusCombo status={item.status} />
|
||||
<p>{item.value}</p>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipPortal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-13
@@ -38,6 +38,7 @@ import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3EnvironmentVariablesPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -183,7 +184,7 @@ export default function Page() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogContent className="md:max-w-2xl lg:max-w-3xl">
|
||||
<DialogHeader>New environment variables</DialogHeader>
|
||||
<Form
|
||||
method="post"
|
||||
@@ -308,20 +309,12 @@ function VariableFields({
|
||||
if (!clipboardData) return;
|
||||
|
||||
let text = clipboardData.getData("text");
|
||||
//replace carriage returns
|
||||
text = text.replace(/\r/g, "");
|
||||
const lines = text.split("\n");
|
||||
if (!text) return;
|
||||
|
||||
const keyValuePairs = lines.flatMap((line) => {
|
||||
if (line.trim().startsWith("#")) return [];
|
||||
|
||||
const split = line.split("=");
|
||||
if (split.length === 2) {
|
||||
return [{ key: split[0], value: split[1] }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const variables = dotenv.parse(text);
|
||||
const keyValuePairs = Object.entries(variables).map(([key, value]) => ({ key, value }));
|
||||
|
||||
//do the default paste
|
||||
if (keyValuePairs.length === 0) return;
|
||||
|
||||
//prevent default pasting
|
||||
|
||||
+52
-33
@@ -1,7 +1,7 @@
|
||||
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { TypedAwait, typeddefer, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
@@ -22,6 +22,8 @@ import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3ProjectPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../../components/ListPagination";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -33,7 +35,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
TaskRunListSearchFilters.parse(s);
|
||||
|
||||
const presenter = new RunListPresenter();
|
||||
const list = await presenter.call({
|
||||
const list = presenter.call({
|
||||
userId,
|
||||
projectSlug: projectParam,
|
||||
tasks,
|
||||
@@ -46,13 +48,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
cursor: cursor,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
list,
|
||||
return typeddefer({
|
||||
data: list,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { list } = useTypedLoaderData<typeof loader>();
|
||||
const { data } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const project = useProject();
|
||||
@@ -64,36 +66,53 @@ export default function Page() {
|
||||
<PageTitle title="Runs" />
|
||||
</NavBar>
|
||||
<PageBody>
|
||||
{list.runs.length === 0 && !list.hasFilters ? (
|
||||
list.possibleTasks.length === 0 ? (
|
||||
<CreateFirstTaskInstructions />
|
||||
) : (
|
||||
<RunTaskInstructions />
|
||||
)
|
||||
) : (
|
||||
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<RunsFilters
|
||||
possibleEnvironments={project.environments}
|
||||
possibleTasks={list.possibleTasks}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="mx-auto flex items-center gap-2">
|
||||
<Spinner />
|
||||
<Paragraph variant="small">Loading runs</Paragraph>
|
||||
</div>
|
||||
|
||||
<TaskRunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={list.hasFilters}
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={data}>
|
||||
{(list) => (
|
||||
<>
|
||||
{list.runs.length === 0 && !list.hasFilters ? (
|
||||
list.possibleTasks.length === 0 ? (
|
||||
<CreateFirstTaskInstructions />
|
||||
) : (
|
||||
<RunTaskInstructions />
|
||||
)
|
||||
) : (
|
||||
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<RunsFilters
|
||||
possibleEnvironments={project.environments}
|
||||
possibleTasks={list.possibleTasks}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskRunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={list.hasFilters}
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-2
@@ -261,10 +261,10 @@ function SchedulesTable({
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<DateTime date={schedule.nextRun} />
|
||||
<DateTime date={schedule.nextRun} timeZone="utc" />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.lastRun ? <DateTime date={schedule.lastRun} /> : "–"}
|
||||
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<div className="flex gap-1">
|
||||
|
||||
+3
-1
@@ -342,7 +342,9 @@ function Timeline({ startTime, duration, inProgress, isError }: TimelineProps) {
|
||||
<DateTimeAccurate date={startTime} />
|
||||
</Paragraph>
|
||||
{state === "pending" ? (
|
||||
<LiveTimer startTime={startTime} className="" />
|
||||
<Paragraph variant="extra-small" className={cn("whitespace-nowrap tabular-nums")}>
|
||||
<LiveTimer startTime={startTime} />
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph variant="small">
|
||||
<DateTimeAccurate
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"typecheck": "tsc -p ./tsconfig.check.json",
|
||||
"db:seed": "node prisma/seed.js",
|
||||
"db:seed:local": "ts-node prisma/seed.ts",
|
||||
"build:db:populate": "esbuild --platform=node --bundle --minify --format=cjs ./prisma/populate.ts --outdir=prisma",
|
||||
"db:populate": "node prisma/populate.js --",
|
||||
"generate:sourcemaps": "remix build --sourcemap",
|
||||
"clean:sourcemaps": "run-s clean:sourcemaps:*",
|
||||
"clean:sourcemaps:public": "rimraf ./build/**/*.map",
|
||||
@@ -58,6 +60,7 @@
|
||||
"@opentelemetry/sdk-trace-base": "^1.22.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.22.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@prisma/instrumentation": "^5.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
@@ -105,6 +108,7 @@
|
||||
"cronstrue": "^2.21.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cuid": "^2.1.8",
|
||||
"dotenv": "^16.4.5",
|
||||
"emails": "workspace:*",
|
||||
"evt": "^2.4.13",
|
||||
"express": "^4.18.1",
|
||||
@@ -136,6 +140,7 @@
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"react-stately": "^3.29.1",
|
||||
"react-use": "^17.4.0",
|
||||
@@ -227,4 +232,4 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Bulk adds data to the database for testing
|
||||
// Call it like this
|
||||
// 1. pnpm run build:db:populate
|
||||
// 2. pnpm run db:populate -- --projectRef=proj_liazlkfgmfcusswwgohl --taskIdentifier=child-task --runCount=100000
|
||||
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
||||
import { prisma } from "../app/db.server";
|
||||
|
||||
async function populate() {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectRef = getArg("projectRef");
|
||||
if (!projectRef) {
|
||||
throw new Error("projectRef is required");
|
||||
}
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
where: {
|
||||
externalRef: projectRef,
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const taskIdentifier = getArg("taskIdentifier");
|
||||
if (!taskIdentifier) {
|
||||
throw new Error("taskIdentifier is required");
|
||||
}
|
||||
|
||||
const runCount = parseInt(getArg("runCount") || "100");
|
||||
|
||||
const task = await prisma.backgroundWorkerTask.findFirst({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
slug: taskIdentifier,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Error("Task not found");
|
||||
}
|
||||
|
||||
const runs = await prisma.taskRun.createMany({
|
||||
data: Array(runCount)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const friendlyId = generateFriendlyId("run");
|
||||
|
||||
return {
|
||||
status: "CANCELED",
|
||||
number: index + 1,
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: project.environments[randomIndex(project.environments)].id,
|
||||
projectId: project.id,
|
||||
taskIdentifier,
|
||||
payload: JSON.stringify({ foo: "bar" }),
|
||||
traceId: "traceId",
|
||||
spanId: "spanId",
|
||||
queue: "task/${taskIdentifier}",
|
||||
};
|
||||
}),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
console.log(`Added ${runs.count} runs`);
|
||||
}
|
||||
|
||||
function getArg(name: string) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let value = "";
|
||||
|
||||
args.forEach((val) => {
|
||||
if (val.startsWith(`--${name}=`)) {
|
||||
value = val.split("=")[1];
|
||||
}
|
||||
});
|
||||
|
||||
return !value ? undefined : value;
|
||||
}
|
||||
|
||||
function randomIndex<T>(array: T[]) {
|
||||
return Math.floor(Math.random() * array.length);
|
||||
}
|
||||
|
||||
populate()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -21,6 +21,7 @@ module.exports = {
|
||||
"random-words",
|
||||
"superjson",
|
||||
],
|
||||
browserNodeBuiltinsPolyfill: { modules: { path: true, os: true, crypto: true } },
|
||||
watchPaths: async () => {
|
||||
return [
|
||||
"../../packages/core/src/**/*",
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@
|
||||
"db:migrate": "turbo run db:migrate:deploy generate",
|
||||
"db:seed": "turbo run db:seed",
|
||||
"db:studio": "turbo run db:studio",
|
||||
"db:populate": "turbo run db:populate",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"i:dev": "infisical run -- turbo run dev --parallel",
|
||||
"format": "prettier . --write --config prettier.config.js",
|
||||
@@ -72,4 +73,4 @@
|
||||
"engine.io-parser@5.2.2": "patches/engine.io-parser@5.2.2.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TaskRun_projectId_idx" ON "TaskRun"("projectId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TaskRun_projectId_taskIdentifier_idx" ON "TaskRun"("projectId", "taskIdentifier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TaskRun_projectId_status_idx" ON "TaskRun"("projectId", "status");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TaskRun_projectId_taskIdentifier_status_idx" ON "TaskRun"("projectId", "taskIdentifier", "status");
|
||||
@@ -1638,6 +1638,11 @@ model TaskRun {
|
||||
@@unique([runtimeEnvironmentId, idempotencyKey])
|
||||
// Task activity graph
|
||||
@@index([projectId, createdAt, taskIdentifier])
|
||||
//Runs list
|
||||
@@index([projectId])
|
||||
@@index([projectId, taskIdentifier])
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, taskIdentifier, status])
|
||||
}
|
||||
|
||||
enum TaskRunStatus {
|
||||
|
||||
Generated
+39
-3
@@ -291,6 +291,9 @@ importers:
|
||||
'@opentelemetry/semantic-conventions':
|
||||
specifier: ^1.22.0
|
||||
version: 1.22.0
|
||||
'@popperjs/core':
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
'@prisma/instrumentation':
|
||||
specifier: ^5.11.0
|
||||
version: 5.11.0
|
||||
@@ -432,6 +435,9 @@ importers:
|
||||
cuid:
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
emails:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/emails
|
||||
@@ -525,6 +531,9 @@ importers:
|
||||
react-hotkeys-hook:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
|
||||
react-popper:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-resizable-panels:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -9515,6 +9524,10 @@ packages:
|
||||
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
|
||||
dev: true
|
||||
|
||||
/@popperjs/core@2.11.8:
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
dev: false
|
||||
|
||||
/@prisma/client@5.4.1(prisma@5.4.1):
|
||||
resolution: {integrity: sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ==}
|
||||
engines: {node: '>=16.13'}
|
||||
@@ -11891,7 +11904,7 @@ packages:
|
||||
cacache: 15.3.0
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.5.3
|
||||
dotenv: 16.4.4
|
||||
dotenv: 16.4.5
|
||||
esbuild: 0.17.6
|
||||
esbuild-plugins-node-modules-polyfill: 1.3.0(esbuild@0.17.6)
|
||||
execa: 5.1.1
|
||||
@@ -11975,7 +11988,7 @@ packages:
|
||||
cacache: 17.1.4
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.5.3
|
||||
dotenv: 16.4.4
|
||||
dotenv: 16.4.5
|
||||
esbuild: 0.17.6
|
||||
esbuild-plugins-node-modules-polyfill: 1.6.1(esbuild@0.17.6)
|
||||
execa: 5.1.1
|
||||
@@ -18372,7 +18385,6 @@ packages:
|
||||
/dotenv@16.4.5:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/dotenv@8.6.0:
|
||||
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
|
||||
@@ -28336,6 +28348,10 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
dev: false
|
||||
|
||||
/react-hotkeys-hook@4.4.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==}
|
||||
peerDependencies:
|
||||
@@ -28356,6 +28372,20 @@ packages:
|
||||
/react-is@18.1.0:
|
||||
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
|
||||
|
||||
/react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
|
||||
peerDependencies:
|
||||
'@popperjs/core': ^2.0.0
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-fast-compare: 3.2.2
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-query@3.39.3(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==}
|
||||
peerDependencies:
|
||||
@@ -33426,6 +33456,12 @@ packages:
|
||||
makeerror: 1.0.12
|
||||
dev: true
|
||||
|
||||
/warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/watchpack@2.4.0:
|
||||
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"db:studio": {
|
||||
"cache": false
|
||||
},
|
||||
"db:populate": {
|
||||
"cache": false
|
||||
},
|
||||
"dev": {
|
||||
"cache": false
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user