fix(frontend): clarify workspace information
This commit is contained in:
@@ -169,7 +169,7 @@ test("offers a button above the workspace list that returns to Level 1", async (
|
|||||||
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("level one explains the read-only Git sequence and the repository update button", async () => {
|
test("level one explains workspace files, shared repositories, and the read-only Git flow", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
server.use(http.post("/api/workspace-registry/pull", () => HttpResponse.json({
|
server.use(http.post("/api/workspace-registry/pull", () => HttpResponse.json({
|
||||||
branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false,
|
branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false,
|
||||||
@@ -178,30 +178,56 @@ test("level one explains the read-only Git sequence and the repository update bu
|
|||||||
|
|
||||||
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
|
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
|
||||||
const overview = screen.getByTestId("workspace-overview");
|
const overview = screen.getByTestId("workspace-overview");
|
||||||
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
|
expect(within(overview).getByRole("heading", { name: "What workspace files tell ThothII" })).toBeVisible();
|
||||||
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
|
expect(within(overview).getByText(/A workspace file describes one data environment/i)).toBeVisible();
|
||||||
const repositoryStep = within(overview).getAllByRole("listitem")[0];
|
expect(within(overview).getByText(/one Git repository can contain many workspaces/i)).toBeVisible();
|
||||||
expect(repositoryStep).toHaveTextContent(/create a workspace repository/i);
|
expect(within(overview).getByText(/each workspace has its own directory/i)).toHaveTextContent(/database target/i);
|
||||||
expect(repositoryStep).toHaveTextContent(/one directory for each workspace/i);
|
expect(within(overview).getByText(/each workspace has its own directory/i)).toHaveTextContent(/Evidence/i);
|
||||||
expect(repositoryStep).toHaveTextContent(/thoth-workspaces\.yaml/i);
|
const repositorySteps = within(overview).getByRole("list");
|
||||||
expect(repositoryStep).toHaveTextContent(/database connection/i);
|
expect(repositorySteps).toHaveClass("gap-0");
|
||||||
expect(repositoryStep).toHaveTextContent(/Evidence sources/i);
|
expect(within(repositorySteps).getAllByRole("listitem")).toHaveLength(3);
|
||||||
expect(repositoryStep).toHaveTextContent(/vector-database collection/i);
|
|
||||||
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
|
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
|
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
|
||||||
);
|
);
|
||||||
|
const repositoryLink = await within(overview).findByRole("link", {
|
||||||
|
name: "git.example.test/analytics/thoth-workspaces",
|
||||||
|
});
|
||||||
|
expect(repositoryLink).toHaveAttribute("href", "https://git.example.test/analytics/thoth-workspaces");
|
||||||
|
expect(repositoryLink).toHaveAttribute("target", "_blank");
|
||||||
|
expect(repositoryLink).toHaveAttribute("rel", "noreferrer");
|
||||||
|
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
|
||||||
expect(within(overview).getAllByText(/managed read-only checkout/i)).toHaveLength(2);
|
expect(within(overview).getAllByText(/managed read-only checkout/i)).toHaveLength(2);
|
||||||
expect(within(overview).getByText(/current active revision remains unchanged/i)).toBeVisible();
|
expect(within(overview).getByText(/current active revision remains unchanged/i)).toBeVisible();
|
||||||
expect(within(overview).getByText(/No workspace selection is required/i)).toBeVisible();
|
expect(within(overview).getByText(/No workspace selection is required/i)).toBeVisible();
|
||||||
expect(await within(overview).findByText("git.example.test/analytics/thoth-workspaces")).toBeVisible();
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
|
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
|
||||||
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
|
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
|
||||||
expect(screen.getByText(/create a workspace repository/i)).toBeInTheDocument();
|
expect(screen.getByText(/A workspace file describes one data environment/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/create a workspace repository/i)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows a local workspace repository as text instead of an invalid web link", async () => {
|
||||||
|
server.use(http.get("/api/workspace-registry/status", () => HttpResponse.json({
|
||||||
|
branch: "main",
|
||||||
|
head: "a".repeat(40),
|
||||||
|
ahead: 0,
|
||||||
|
behind: 0,
|
||||||
|
degraded: false,
|
||||||
|
repository: {
|
||||||
|
host: "local",
|
||||||
|
repository: "configured-repository",
|
||||||
|
transport: "local",
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
renderManager();
|
||||||
|
|
||||||
|
const overview = await screen.findByTestId("workspace-overview");
|
||||||
|
expect(await within(overview).findByText("local/configured-repository")).toBeVisible();
|
||||||
|
expect(within(overview).queryByRole("link", { name: "local/configured-repository" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("workspace-specific commands remain isolated until a workspace is selected", async () => {
|
test("workspace-specific commands remain isolated until a workspace is selected", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderManager();
|
renderManager();
|
||||||
@@ -209,10 +235,15 @@ test("workspace-specific commands remain isolated until a workspace is selected"
|
|||||||
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
|
||||||
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
||||||
|
|
||||||
|
expect(await screen.findByRole("heading", { name: "About this workspace" })).toBeVisible();
|
||||||
expect(await screen.findByRole("heading", { name: "Workspace-specific actions" })).toBeVisible();
|
expect(await screen.findByRole("heading", { name: "Workspace-specific actions" })).toBeVisible();
|
||||||
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
|
expect(screen.getByText(/one workspace in the shared repository/i)).toHaveTextContent(/workspace.yaml/i);
|
||||||
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
|
expect(screen.getByText(/one workspace in the shared repository/i)).toHaveTextContent(/Evidence/i);
|
||||||
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
|
expect(screen.getByText(/exact version currently activated by ThothII/i)).toBeVisible();
|
||||||
|
expect(screen.getByText(/does not modify the repository/i)).toBeVisible();
|
||||||
|
expect(screen.getByText(/confirms that workspace.yaml and the directories/i)).toBeVisible();
|
||||||
|
expect(screen.getByText(/reach the data warehouse and Evidence sources/i)).toBeVisible();
|
||||||
|
expect(screen.getByText(/credentials only for the duration of the test/i)).toBeVisible();
|
||||||
const databaseField = screen.getByText("Database").parentElement;
|
const databaseField = screen.getByText("Database").parentElement;
|
||||||
expect(databaseField).not.toBeNull();
|
expect(databaseField).not.toBeNull();
|
||||||
expect(databaseField).toHaveTextContent("engine: postgres");
|
expect(databaseField).toHaveTextContent("engine: postgres");
|
||||||
|
|||||||
@@ -386,6 +386,9 @@ export function WorkspaceManager({
|
|||||||
const repositoryLabel = repository
|
const repositoryLabel = repository
|
||||||
? `${repository.host}/${repository.repository}`
|
? `${repository.host}/${repository.repository}`
|
||||||
: "the repository configured for this ThothII installation";
|
: "the repository configured for this ThothII installation";
|
||||||
|
const repositoryWebUrl = repository && repository.transport !== "local"
|
||||||
|
? `https://${repository.host}/${repository.repository}`
|
||||||
|
: undefined;
|
||||||
const runtime = runtimeQuery.data;
|
const runtime = runtimeQuery.data;
|
||||||
const hasEnteredSecrets = Object.values(secretValues).some((value) => value.length > 0);
|
const hasEnteredSecrets = Object.values(secretValues).some((value) => value.length > 0);
|
||||||
|
|
||||||
@@ -459,16 +462,34 @@ export function WorkspaceManager({
|
|||||||
<section data-testid="workspace-overview" className="mx-auto max-w-3xl space-y-5">
|
<section data-testid="workspace-overview" className="mx-auto max-w-3xl space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<p className="thot-label">Level 1 · Repository</p>
|
<p className="thot-label">Level 1 · Repository</p>
|
||||||
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
|
<h2 className="font-heading text-xl font-semibold">What workspace files tell ThothII</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 text-sm leading-6 text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
A workspace file describes one data environment. Its <code>workspace.yaml</code> gives ThothII the workspace identity and language, the database target and schema it may query, the Evidence sources that support analysis, and the processing settings to apply. Runtime credentials are stored separately from Git.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
One Git repository can contain many workspaces. At its root, <code>thoth-workspaces.yaml</code> lists them, and each workspace has its own directory, <code>workspace.yaml</code>, database target and access method, and Evidence sources. This keeps separate data environments and supporting material distinct while versioning them together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-heading font-semibold text-foreground">How this installation uses the repository</h3>
|
||||||
|
<ol className="mt-1 grid list-decimal gap-0 pl-5 text-sm leading-6 text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads{" "}
|
||||||
|
{repositoryWebUrl ? (
|
||||||
|
<a href={repositoryWebUrl} target="_blank" rel="noreferrer" className="break-all font-medium text-foreground underline underline-offset-4 hover:text-primary">{repositoryLabel}</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium text-foreground">{repositoryLabel}</span>
|
||||||
|
)}{" "}
|
||||||
|
on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.
|
||||||
|
</li>
|
||||||
|
<li>ThothII fetches the configured branch into its managed read-only checkout, reads the workspaces listed in the repository, and validates the complete candidate revision before activating it.</li>
|
||||||
|
<li>The active revision stays read-only. ThothII never edits, commits, pushes, or publishes workspace files; if validation fails, the previous valid revision stays active.</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
|
|
||||||
<li><span className="font-medium text-foreground">Create a workspace repository</span> in any local directory you choose. Add one directory for each workspace you want ThothII to manage. At the repository root, <code>thoth-workspaces.yaml</code> lists those workspaces; each workspace directory contains its own <code>workspace.yaml</code>, which declares the database connection, the Evidence sources, and the ThothII vector-database collection used during the process.</li>
|
|
||||||
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
|
|
||||||
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
|
|
||||||
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
|
|
||||||
</ol>
|
|
||||||
<p className="text-sm leading-6 text-muted-foreground">
|
<p className="text-sm leading-6 text-muted-foreground">
|
||||||
Follow the <a href={workspaceAuthoringGuideUrl} target="_blank" rel="noreferrer" className="font-medium text-foreground underline underline-offset-4 hover:text-primary">workspace authoring instructions on GitHub</a> for the required layout and validation rules.
|
The <a href={workspaceAuthoringGuideUrl} target="_blank" rel="noreferrer" className="font-medium text-foreground underline underline-offset-4 hover:text-primary">workspace authoring instructions on GitHub</a> explain the required layout and validation rules for people who maintain the repository.
|
||||||
</p>
|
</p>
|
||||||
{canManageWorkspace && <div className="rounded-lg border border-border bg-muted/30 p-4">
|
{canManageWorkspace && <div className="rounded-lg border border-border bg-muted/30 p-4">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
@@ -486,7 +507,7 @@ export function WorkspaceManager({
|
|||||||
You can inspect workspaces. Workspace updates, validation, and connection tests require workspace management permission.
|
You can inspect workspaces. Workspace updates, validation, and connection tests require workspace management permission.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-muted-foreground">Select a workspace from the left only for workspace-specific validation, runtime credentials, and connection tests.</p>
|
<p className="text-sm leading-6 text-muted-foreground">Selecting a workspace from the left opens its own revision, database and Evidence configuration, runtime requirements, validation, and connection checks.</p>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<section className="mx-auto max-w-4xl space-y-5">
|
<section className="mx-auto max-w-4xl space-y-5">
|
||||||
@@ -503,9 +524,14 @@ export function WorkspaceManager({
|
|||||||
|
|
||||||
{detailQuery.data && runtime && (
|
{detailQuery.data && runtime && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="max-w-3xl">
|
||||||
<h3 className="font-heading text-lg font-semibold">Workspace-specific actions</h3>
|
<h3 className="font-heading text-lg font-semibold">About this workspace</h3>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">The actions below apply only to <span className="font-medium text-foreground">{selectedSummary.displayName}</span>. ThothII reads this revision without modifying or publishing it.</p>
|
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||||
|
<span className="font-medium text-foreground">{selectedSummary.displayName}</span> is one workspace in the shared repository. Its workspace.yaml defines the database target, processing settings, and Evidence sources for this data environment. Other workspaces in the same repository can use different databases and keep their Evidence separate.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||||
|
The active revision shown below is the exact version currently activated by ThothII. Reading or checking it does not modify the repository. Source changes are made in Git and become available after a new revision is fetched, validated, and activated.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
|
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
|
||||||
@@ -522,10 +548,15 @@ export function WorkspaceManager({
|
|||||||
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-heading text-lg font-semibold">Workspace-specific actions</h3>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-muted-foreground">The validation and connection checks below apply only to <span className="font-medium text-foreground">{selectedSummary.displayName}</span> and read its active revision without changing or publishing it.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 lg:grid-cols-2">
|
<div className="grid gap-3 lg:grid-cols-2">
|
||||||
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
|
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
|
||||||
<h4 className="font-heading font-semibold">Validate workspace source</h4>
|
<h4 className="font-heading font-semibold">Validate workspace source</h4>
|
||||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
|
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Confirms that workspace.yaml and the directories belonging to this workspace follow the supported format. This identifies source problems before activation, without changing repository files.</p>
|
||||||
{validationNotice && (
|
{validationNotice && (
|
||||||
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||||
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
|
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
|
||||||
@@ -546,7 +577,7 @@ export function WorkspaceManager({
|
|||||||
</div>
|
</div>
|
||||||
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
|
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
|
||||||
<h4 className="font-heading font-semibold">Test workspace connections</h4>
|
<h4 className="font-heading font-semibold">Test workspace connections</h4>
|
||||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
|
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Verifies that ThothII can reach the data warehouse and Evidence sources configured for this workspace. It decrypts credentials only for the duration of the test and deletes temporary files when the check finishes.</p>
|
||||||
{connectionNotice && (
|
{connectionNotice && (
|
||||||
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||||
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
|
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
|
||||||
@@ -589,7 +620,7 @@ export function WorkspaceManager({
|
|||||||
<KeyRound className="mt-0.5 size-5 text-primary" />
|
<KeyRound className="mt-0.5 size-5 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-heading font-semibold">Runtime secrets</h3>
|
<h3 className="font-heading font-semibold">Runtime secrets</h3>
|
||||||
<p className="mt-1 text-sm leading-5 text-muted-foreground">Enter only new or replacement values. Stored values are never displayed. Saving replaces the selected secret and clears the form field.</p>
|
<p className="mt-1 text-sm leading-5 text-muted-foreground">Some connectors need credentials at runtime. ThothII stores these values separately from Git and never displays stored values. Enter a value only when adding or replacing a credential for this workspace.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{runtime.requirements.length === 0 ? (
|
{runtime.requirements.length === 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user