Skip to content
Closed
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/superset
VITE_DEV_SERVER_PORT=4927
WEBSITE_URL=http://localhost:3001
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ mise.toml
# Superset
# Ignore .superset directory except for config and scripts
.superset/*
!.superset/setup.json
!.superset/config.json
!.superset/setup.sh
!.superset/teardown.sh

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ open apps/desktop/release

### Usage

For each parallel tasks, Superset uses git worktrees to clone a new branch on your machine. Automate copying env variables, installing dependencies, etc. through a setup script (`./superset/setup.json`)
For each parallel tasks, Superset uses git worktrees to clone a new branch on your machine. Automate copying env variables, installing dependencies, etc. through a config file (`.superset/config.json`)

<div align="center">
<img width="600" alt="Creating a worktree" src="assets/create-worktree.gif" />
Expand Down
88 changes: 88 additions & 0 deletions apps/desktop/src/lib/trpc/routers/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { db } from "main/lib/db";
import { CONFIG_FILE_NAME, PROJECT_SUPERSET_DIR_NAME } from "shared/constants";
import { z } from "zod";
import { publicProcedure, router } from "../..";

function configExists(mainRepoPath: string): boolean {
const configPath = join(
mainRepoPath,
PROJECT_SUPERSET_DIR_NAME,
CONFIG_FILE_NAME,
);
return existsSync(configPath);
}

const CONFIG_TEMPLATE = `{
"setup": [],
"teardown": []
}
`;

function getConfigPath(mainRepoPath: string): string {
return join(mainRepoPath, PROJECT_SUPERSET_DIR_NAME, CONFIG_FILE_NAME);
}

function ensureConfigExists(mainRepoPath: string): string {
const configPath = getConfigPath(mainRepoPath);
const supersetDir = join(mainRepoPath, PROJECT_SUPERSET_DIR_NAME);

if (!existsSync(configPath)) {
// Create .superset directory if it doesn't exist
if (!existsSync(supersetDir)) {
mkdirSync(supersetDir, { recursive: true });
}
// Create config.json with template
writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
}

return configPath;
}

export const createConfigRouter = () => {
return router({
// Check if we should show the config toast for a project
shouldShowConfigToast: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(({ input }) => {
const project = db.data.projects.find((p) => p.id === input.projectId);
if (!project) {
return false;
}

// Don't show if already dismissed or if config exists
if (project.configToastDismissed) {
return false;
}

return !configExists(project.mainRepoPath);
}),

// Mark the config toast as dismissed for a project
dismissConfigToast: publicProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input }) => {
await db.update((data) => {
const project = data.projects.find((p) => p.id === input.projectId);
if (project) {
project.configToastDismissed = true;
}
});
return { success: true };
}),

// Get the config file path (creates it if it doesn't exist)
getConfigFilePath: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(({ input }) => {
const project = db.data.projects.find((p) => p.id === input.projectId);
if (!project) {
return null;
}
return ensureConfigExists(project.mainRepoPath);
}),
});
};

export type ConfigRouter = ReturnType<typeof createConfigRouter>;
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { ConfigRouter } from "./config";
export { createConfigRouter } from "./config";
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BrowserWindow } from "electron";
import { router } from "..";
import { createConfigRouter } from "./config";
import { createExternalRouter } from "./external";
import { createNotificationsRouter } from "./notifications";
import { createProjectsRouter } from "./projects";
Expand All @@ -21,6 +22,7 @@ export const createAppRouter = (window: BrowserWindow) => {
notifications: createNotificationsRouter(),
external: createExternalRouter(),
settings: createSettingsRouter(),
config: createConfigRouter(),
});
};

Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ function extractRepoName(urlInput: string): string | null {

export const createProjectsRouter = (window: BrowserWindow) => {
return router({
get: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }): Project | null => {
return db.data.projects.find((p) => p.id === input.id) ?? null;
}),

getRecents: publicProcedure.query((): Project[] => {
return db.data.projects
.slice()
Expand Down
14 changes: 9 additions & 5 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { CONFIG_FILE_NAME, PROJECT_SUPERSET_DIR_NAME } from "shared/constants";
import { loadSetupConfig } from "./setup";

const TEST_DIR = join(__dirname, ".test-tmp");
Expand All @@ -9,7 +10,7 @@ const MAIN_REPO = join(TEST_DIR, "main-repo");
describe("loadSetupConfig", () => {
beforeEach(() => {
// Create test directories
mkdirSync(join(MAIN_REPO, ".superset"), { recursive: true });
mkdirSync(join(MAIN_REPO, PROJECT_SUPERSET_DIR_NAME), { recursive: true });
});

afterEach(() => {
Expand All @@ -19,7 +20,7 @@ describe("loadSetupConfig", () => {
}
});

test("returns null when setup.json does not exist", () => {
test("returns null when config.json does not exist", () => {
const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
});
Expand All @@ -30,7 +31,7 @@ describe("loadSetupConfig", () => {
};

writeFileSync(
join(MAIN_REPO, ".superset", "setup.json"),
join(MAIN_REPO, PROJECT_SUPERSET_DIR_NAME, CONFIG_FILE_NAME),
JSON.stringify(setupConfig),
);

Expand All @@ -39,15 +40,18 @@ describe("loadSetupConfig", () => {
});

test("returns null for invalid JSON", () => {
writeFileSync(join(MAIN_REPO, ".superset", "setup.json"), "{ invalid json");
writeFileSync(
join(MAIN_REPO, PROJECT_SUPERSET_DIR_NAME, CONFIG_FILE_NAME),
"{ invalid json",
);

const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
});

test("validates setup field must be an array", () => {
writeFileSync(
join(MAIN_REPO, ".superset", "setup.json"),
join(MAIN_REPO, PROJECT_SUPERSET_DIR_NAME, CONFIG_FILE_NAME),
JSON.stringify({ setup: "not-an-array" }),
);

Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { CONFIG_FILE_NAME, PROJECT_SUPERSET_DIR_NAME } from "shared/constants";
import type { SetupConfig } from "shared/types";

export function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
const configPath = join(mainRepoPath, ".superset", "setup.json");
const configPath = join(
mainRepoPath,
PROJECT_SUPERSET_DIR_NAME,
CONFIG_FILE_NAME,
);

if (!existsSync(configPath)) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { CONFIG_FILE_NAME, PROJECT_SUPERSET_DIR_NAME } from "shared/constants";
import type { SetupConfig } from "shared/types";

const TEARDOWN_TIMEOUT_MS = 60_000; // 60 seconds
Expand All @@ -11,7 +12,11 @@ export interface TeardownResult {
}

function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
const configPath = join(mainRepoPath, ".superset", "setup.json");
const configPath = join(
mainRepoPath,
PROJECT_SUPERSET_DIR_NAME,
CONFIG_FILE_NAME,
);

if (!existsSync(configPath)) {
return null;
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const createWorkspacesRouter = () => {
workspace,
initialCommands: setupConfig?.setup || null,
worktreePath,
projectId: project.id,
};
}),

Expand Down Expand Up @@ -210,9 +211,26 @@ export const createWorkspacesRouter = () => {
);
}

const project = db.data.projects.find(
(p) => p.id === workspace.projectId,
);
const worktree = db.data.worktrees.find(
(wt) => wt.id === workspace.worktreeId,
);

return {
...workspace,
worktreePath: getWorktreePath(workspace.worktreeId) ?? "",
project: project
? {
id: project.id,
name: project.name,
mainRepoPath: project.mainRepoPath,
}
: null,
worktree: worktree
? { branch: worktree.branch, gitStatus: worktree.gitStatus }
: null,
};
}),

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/lib/db/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Project {
tabOrder: number | null;
lastOpenedAt: number;
createdAt: number;
configToastDismissed?: boolean;
}

export interface GitStatus {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { cn } from "@superset/ui/utils";
import { OpenInButton } from "renderer/components/OpenInButton";
import {
CONFIG_FILE_NAME,
CONFIG_TEMPLATE,
PROJECT_SUPERSET_DIR_NAME,
WEBSITE_URL,
} from "shared/constants";
import { Button } from "@superset/ui/button";
import { HiArrowTopRightOnSquare } from "react-icons/hi2";
export interface ConfigFilePreviewProps {
projectName: string;
configFilePath?: string;
className?: string;
}

export function ConfigFilePreview({
projectName,
configFilePath,
className,
}: ConfigFilePreviewProps) {
const handleLearnMore = () => {
window.open(`${WEBSITE_URL}/scripts`, "_blank");
};

return (
<>
<div
className={cn(
"rounded-lg border border-border bg-card overflow-hidden",
className,
)}
>
<div className="flex items-center justify-between gap-4 px-4 py-3 border-b border-border">
<span className="text-sm text-muted-foreground font-mono truncate">
{projectName}/{PROJECT_SUPERSET_DIR_NAME}/{CONFIG_FILE_NAME}
</span>
<OpenInButton path={configFilePath} label={CONFIG_FILE_NAME} />
</div>

<div className="p-4 bg-background/50">
<pre className="text-sm font-mono text-foreground leading-relaxed">
{CONFIG_TEMPLATE}
</pre>
</div>
</div>

<div className="mt-4">
<Button
variant="outline"
size="sm"
onClick={handleLearnMore}
className="gap-2"
>
Learn how to use scripts
<HiArrowTopRightOnSquare className="h-4 w-4" />
</Button>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
ConfigFilePreview,
type ConfigFilePreviewProps,
} from "./ConfigFilePreview";
Loading