Skip to content
Open
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
29 changes: 27 additions & 2 deletions src/components/Project/AddToProjectDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { pointerTo } from "@/services/localPipelines/localPipelinesService";
import type { ProjectPipelineRef } from "@/services/projects/pipelineResource";
import { useAddResourceToProject } from "@/services/projects/useProjectResources";
import { useReachableProjects } from "@/services/projects/useReachableProjects";

Expand Down Expand Up @@ -40,10 +41,13 @@ function mockProjects(items = [{ id: "project-1", name: "Churn work" }]) {
} as unknown as ReturnType<typeof useReachableProjects>);
}

const dialog = (memberProjectIds: string[] = []) =>
const dialog = (
memberProjectIds: string[] = [],
pipeline: ProjectPipelineRef = { source: "local", name: "Churn model" },
) =>
render(
<AddToProjectDialog
pipelineName="Churn model"
pipeline={pipeline}
memberProjectIds={memberProjectIds}
onOpenChange={onOpenChange}
onAdded={onAdded}
Expand Down Expand Up @@ -76,6 +80,27 @@ describe("AddToProjectDialog", () => {
});
});

it("files a backend pipeline by its id rather than as a browser pointer", async () => {
dialog([], {
source: "remote",
pipelineId: "pipeline-7",
name: "Churn model",
});

await userEvent.click(screen.getByRole("button", { name: /Churn work/ }));

await waitFor(() => expect(mutate).toHaveBeenCalled());
expect(mutate.mock.calls[0][0]).toEqual({
projectId: "project-1",
input: {
entity: "pipeline",
entityId: "pipeline-7",
name: "Churn model",
},
});
expect(pointerTo).not.toHaveBeenCalled();
});

/** Attribution follows membership, so the project just joined is where runs go. */
it("sends the runs of this tab to the project it joined", async () => {
dialog();
Expand Down
21 changes: 17 additions & 4 deletions src/components/Project/AddToProjectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,37 @@ import { useEffect } from "react";
import useToastNotification from "@/hooks/useToastNotification";
import { useAnalytics } from "@/providers/AnalyticsProvider";
import { pointerTo } from "@/services/localPipelines/localPipelinesService";
import {
type ProjectPipelineRef,
remotePipelineResourceInput,
} from "@/services/projects/pipelineResource";
import {
DescriptorTooLargeError,
localPipelineResourceInput,
} from "@/services/projects/resourceDescriptor";
import type { CreateResourceInput } from "@/services/projects/types";
import { useAddResourceToProject } from "@/services/projects/useProjectResources";
import { useReachableProjects } from "@/services/projects/useReachableProjects";

import { PickFromListDialog } from "./PickFromListDialog";

interface AddToProjectDialogProps {
pipelineName: string;
pipeline: ProjectPipelineRef;
memberProjectIds: readonly string[];
onOpenChange: (open: boolean) => void;
onAdded: (projectId: string) => void;
}

async function resourceInputFor(
pipeline: ProjectPipelineRef,
): Promise<CreateResourceInput> {
return pipeline.source === "remote"
? remotePipelineResourceInput(pipeline.pipelineId, pipeline.name)
: localPipelineResourceInput(await pointerTo(pipeline.name));
}

export function AddToProjectDialog({
pipelineName,
pipeline,
memberProjectIds,
onOpenChange,
onAdded,
Expand All @@ -44,7 +57,7 @@ export function AddToProjectDialog({
const add = async (projectId: string) => {
let input;
try {
input = localPipelineResourceInput(await pointerTo(pipelineName));
input = await resourceInputFor(pipeline);
} catch (problem) {
notify(
problem instanceof DescriptorTooLargeError
Expand All @@ -71,7 +84,7 @@ export function AddToProjectDialog({
return (
<PickFromListDialog
title="Add to a project"
helpText={`Runs of "${pipelineName}" can then be attributed to the project you pick.`}
helpText={`Runs of "${pipeline.name}" can then be attributed to the project you pick.`}
noun="project"
icon="Folder"
items={items}
Expand Down
51 changes: 39 additions & 12 deletions src/components/Project/ProjectPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import type { ProjectPipelineRef } from "@/services/projects/pipelineResource";
import type { PipelineProjectMembership } from "@/services/projects/usePipelineProjects";
import { usePipelineProjects } from "@/services/projects/usePipelineProjects";
import { useDeleteProjectResource } from "@/services/projects/useProjectResources";

import { ProjectPicker } from "./ProjectPicker";
import { useRunProjectContext } from "./useRunProjectContext";

const CHURN_MODEL: ProjectPipelineRef = {
source: "local",
name: "Churn model",
};

const setProjectId = vi.fn();
const removeResource = vi.fn();
const notify = vi.fn();
Expand Down Expand Up @@ -93,14 +99,14 @@ describe("ProjectPicker", () => {
it("shows nothing at all while projects are switched off", () => {
given({ enabled: false });

const { container } = render(<ProjectPicker pipelineName="Churn model" />);
const { container } = render(<ProjectPicker pipeline={CHURN_MODEL} />);

expect(container).toBeEmptyDOMElement();
});

/** The old chip only appeared once a project was set, so there was nothing to click. */
it("offers itself on a pipeline that belongs to no project", () => {
render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);

expect(
screen.getByRole("button", { name: "Project for runs: No project" }),
Expand All @@ -110,7 +116,7 @@ describe("ProjectPicker", () => {
it("names the project the runs of this tab go to", () => {
given({ projectId: "project-1", projectName: "Churn work" });

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);

expect(
screen.getByRole("button", { name: "Project for runs: Churn work" }),
Expand All @@ -127,7 +133,7 @@ describe("ProjectPicker", () => {
],
});

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();

expect(
Expand All @@ -148,7 +154,7 @@ describe("ProjectPicker", () => {
],
});

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();
await userEvent.click(
screen.getByRole("menuitemradio", { name: "Q3 models" }),
Expand All @@ -160,7 +166,7 @@ describe("ProjectPicker", () => {
it("stops sending them anywhere", async () => {
given({ projectId: "project-1", projectName: "Churn work" });

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();
await userEvent.click(
screen.getByRole("menuitemradio", { name: "No project" }),
Expand All @@ -176,7 +182,7 @@ describe("ProjectPicker", () => {
it("lists a chosen project the pipeline is not in", async () => {
given({ projectId: "project-9", projectName: "Somebody else's" });

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();

expect(
Expand All @@ -187,7 +193,7 @@ describe("ProjectPicker", () => {
it("offers no removal from a project the pipeline is not in", async () => {
given({ projectId: "project-9", projectName: "Somebody else's" });

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();

expect(screen.queryByText(/^Remove from/)).not.toBeInTheDocument();
Expand All @@ -200,7 +206,7 @@ describe("ProjectPicker", () => {
memberships: [membership("project-1", "Churn work")],
});

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();
await userEvent.click(screen.getByText("Remove from Churn work"));
await userEvent.click(
Expand All @@ -223,7 +229,7 @@ describe("ProjectPicker", () => {
memberships: [membership("project-1", "Churn work")],
});

render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();
await userEvent.click(screen.getByText("Remove from Churn work"));

Expand All @@ -234,10 +240,31 @@ describe("ProjectPicker", () => {
});

it("waits to be asked before reading every project's contents", () => {
render(<ProjectPicker pipelineName="Churn model" />);
render(<ProjectPicker pipeline={CHURN_MODEL} />);

expect(usePipelineProjects).toHaveBeenCalledWith("Churn model", {
expect(usePipelineProjects).toHaveBeenCalledWith(CHURN_MODEL, {
enabled: false,
});
});

it("keeps looking up the pipeline's projects while it is being added to one", async () => {
render(<ProjectPicker pipeline={CHURN_MODEL} />);
await open();
await userEvent.click(screen.getByText("Add to a project..."));

expect(screen.getByText("add to project dialog")).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: /Add to a project/ }),
).not.toBeInTheDocument();
expect(usePipelineProjects).toHaveBeenLastCalledWith(CHURN_MODEL, {
enabled: true,
});
});

it("offers no adding while there is no pipeline to add", async () => {
render(<ProjectPicker pipeline={undefined} />);
await open();

expect(screen.queryByText("Add to a project...")).not.toBeInTheDocument();
});
});
15 changes: 8 additions & 7 deletions src/components/Project/ProjectPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Icon } from "@/components/ui/icon";
import { InlineStack } from "@/components/ui/layout";
import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/typography";
import type { ProjectPipelineRef } from "@/services/projects/pipelineResource";
import { usePipelineProjects } from "@/services/projects/usePipelineProjects";
import { tracking } from "@/utils/tracking";

Expand All @@ -25,16 +26,16 @@ import { useConfirmedResourceRemoval } from "./useConfirmedResourceRemoval";
import { useRunProjectContext } from "./useRunProjectContext";

interface ProjectPickerProps {
pipelineName: string | undefined;
pipeline: ProjectPipelineRef | undefined;
}

export function ProjectPicker({ pipelineName }: ProjectPickerProps) {
export function ProjectPicker({ pipeline }: ProjectPickerProps) {
const { enabled, projectId, projectName, setProjectId } =
useRunProjectContext();
const [open, setOpen] = useState(false);
const [addOpen, setAddOpen] = useState(false);
const { memberships, isPending, error } = usePipelineProjects(pipelineName, {
enabled: open,
const { memberships, isPending, error } = usePipelineProjects(pipeline, {
enabled: open || addOpen,
});
const { confirmAndRemove, confirmation: confirmationProps } =
useConfirmedResourceRemoval({
Expand Down Expand Up @@ -118,7 +119,7 @@ export function ProjectPicker({ pipelineName }: ProjectPickerProps) {
</InlineStack>
)}

{pipelineName && (
{pipeline && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
Expand Down Expand Up @@ -146,9 +147,9 @@ export function ProjectPicker({ pipelineName }: ProjectPickerProps) {
</DropdownMenuContent>
</DropdownMenu>

{addOpen && pipelineName && (
{addOpen && pipeline && (
<AddToProjectDialog
pipelineName={pipelineName}
pipeline={pipeline}
memberProjectIds={memberships.map(
(membership) => membership.project.id,
)}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Project/RunProjectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Paragraph } from "@/components/ui/typography";
import { localProjectPipeline } from "@/services/projects/pipelineResource";
import { usePipelineProjects } from "@/services/projects/usePipelineProjects";

import { NO_PROJECT, projectOptions } from "./projectOptions";
Expand All @@ -30,7 +31,7 @@ export function RunProjectField({
}: RunProjectFieldProps) {
const { enabled, projectName } = useRunProjectContext();
const { memberships } = usePipelineProjects(
enabled ? pipelineName : undefined,
enabled ? localProjectPipeline(pipelineName) : undefined,
);

const options = projectOptions(memberships, value, projectName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import GoogleCloudSubmissionDialog from "@/components/shared/Submitters/GoogleCl
import TangleSubmitter from "@/components/shared/Submitters/Tangle/TangleSubmitter";
import { BlockStack } from "@/components/ui/layout";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
import { localProjectPipeline } from "@/services/projects/pipelineResource";
import { isFixableIssue } from "@/utils/validations";

import { RecentExecutionsButton } from "../components/RecentExecutionsButton";
Expand All @@ -30,7 +31,7 @@ const RunsAndSubmission = () => {
<SidebarSection title="Runs & Submissions" headerAction={showMoreButton}>
<BlockStack as="ul" gap="1">
<li className="w-full px-2.5">
<ProjectPicker pipelineName={componentSpec?.name} />
<ProjectPicker pipeline={localProjectPipeline(componentSpec?.name)} />
</li>
<li className="w-full">
{isAuthorized ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CanvasMenuBar,
} from "@/routes/v2/shared/components/CanvasMenuBar";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import { projectPipelineOf } from "@/services/projects/pipelineResource";
import { tracking } from "@/utils/tracking";

import { AutoSaveIndicator } from "./components/AutoSaveIndicator";
Expand Down Expand Up @@ -158,9 +159,11 @@ export const EditorMenuBar = observer(function EditorMenuBar() {
>
{displayMenu && (
<>
{!isRemote && (
<ProjectPicker pipelineName={activePipeline?.storageKey} />
)}
<ProjectPicker
pipeline={
activePipeline ? projectPipelineOf(activePipeline) : undefined
}
/>
<QuickRunButton />
<div className="w-px h-5 bg-stone-700" />
</>
Expand Down
Loading
Loading