Skip to content
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
36 changes: 25 additions & 11 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,31 @@ export function Layout() {

{/* Footer */}
<div className="border-t border-gray-200 p-3 dark:border-gray-800">
<p className="text-[11px] text-gray-400 dark:text-gray-600">
Powered by{" "}
<a
href="https://github.com/opendecree/decree"
target="_blank"
rel="noopener noreferrer"
className="hover:text-gray-600 hover:underline dark:hover:text-gray-400"
>
OpenDecree
</a>
</p>
<div className="space-y-1">
<p className="text-[11px] text-gray-400 dark:text-gray-600">
v{__APP_VERSION__}
{" · "}
<a
href="https://github.com/opendecree/decree"
target="_blank"
rel="noopener noreferrer"
className="hover:text-gray-600 hover:underline dark:hover:text-gray-400"
>
Docs
</a>
</p>
<p className="text-[11px] text-gray-400 dark:text-gray-600">
Powered by{" "}
<a
href="https://github.com/opendecree/decree"
target="_blank"
rel="noopener noreferrer"
className="hover:text-gray-600 hover:underline dark:hover:text-gray-400"
>
OpenDecree
</a>
</p>
</div>
</div>
</nav>

Expand Down
55 changes: 55 additions & 0 deletions src/components/__tests__/Layout.full.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { AuthContext } from "../../lib/auth";
import { Layout } from "../Layout";

vi.mock("../../lib/config", () => ({
config: { layoutMode: "full", appName: "Test App", logoUrl: "" },
}));

beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi
.fn()
.mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() }),
});
});

function renderLayout() {
return render(
<MemoryRouter>
<AuthContext value={{ auth: { subject: "admin", role: "superadmin" }, setAuth: () => {} }}>
<Layout />
</AuthContext>
</MemoryRouter>,
);
}

describe("Layout — full mode", () => {
it("renders sidebar navigation", () => {
renderLayout();
expect(screen.getByRole("navigation")).toBeInTheDocument();
});

it("shows Home nav link", () => {
renderLayout();
expect(screen.getByText("Home")).toBeInTheDocument();
});

it("renders footer with version number", () => {
renderLayout();
expect(screen.getByText(/^v\d/)).toBeInTheDocument();
});

it("renders footer Docs link", () => {
renderLayout();
expect(screen.getByRole("link", { name: "Docs" })).toBeInTheDocument();
});

it("renders footer Powered by OpenDecree link", () => {
renderLayout();
expect(screen.getByRole("link", { name: "OpenDecree" })).toBeInTheDocument();
});
});
89 changes: 89 additions & 0 deletions src/components/__tests__/Layout.single-tenant.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { AuthContext } from "../../lib/auth";
import { Layout } from "../Layout";

vi.mock("../../lib/config", () => ({
config: {
layoutMode: "single-tenant",
tenantId: "00000000-0000-0000-0000-000000000001",
appName: "Test App",
logoUrl: "",
},
}));

beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi
.fn()
.mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() }),
});
});

function renderLayout() {
return render(
<MemoryRouter>
<AuthContext
value={{
auth: {
subject: "admin",
role: "admin",
tenantId: "00000000-0000-0000-0000-000000000001",
},
setAuth: () => {},
}}
>
<Layout />
</AuthContext>
</MemoryRouter>,
);
}

describe("Layout — single-tenant mode", () => {
it("renders sidebar navigation", () => {
renderLayout();
expect(screen.getByRole("navigation")).toBeInTheDocument();
});

it("does not show Home nav link", () => {
renderLayout();
expect(screen.queryByText("Home")).not.toBeInTheDocument();
});

it("shows Configuration nav item", () => {
renderLayout();
expect(screen.getByText("Configuration")).toBeInTheDocument();
});

it("shows History nav item", () => {
renderLayout();
expect(screen.getByText("History")).toBeInTheDocument();
});

it("shows Audit Log nav item", () => {
renderLayout();
expect(screen.getByText("Audit Log")).toBeInTheDocument();
});

it("shows Usage nav item", () => {
renderLayout();
expect(screen.getByText("Usage")).toBeInTheDocument();
});

it("renders footer with version number", () => {
renderLayout();
expect(screen.getByText(/^v\d/)).toBeInTheDocument();
});

it("renders footer Docs link", () => {
renderLayout();
expect(screen.getByRole("link", { name: "Docs" })).toBeInTheDocument();
});

it("renders footer Powered by OpenDecree link", () => {
renderLayout();
expect(screen.getByRole("link", { name: "OpenDecree" })).toBeInTheDocument();
});
});
35 changes: 30 additions & 5 deletions src/pages/tenants/TenantDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export function TenantDetail() {
const [pendingChanges, setPendingChanges] = useState<Map<string, string>>(new Map());
const [description, setDescription] = useState("");

const productMode =
appConfig.layoutMode === "single-tenant" || appConfig.layoutMode === "config-only";

const fields = schema?.fields ?? [];
const configValues = config?.values ?? [];
const locks = locksData?.locks ?? [];
Expand Down Expand Up @@ -416,6 +419,7 @@ export function TenantDetail() {
lastChanged={lastChangedMap.get(field.path ?? "")}
editing={editing}
showLocks={canManageLocks(auth.role)}
productMode={productMode}
onChange={(v) => handleChange(field.path ?? "", v)}
onUndo={() => handleUndo(field.path ?? "")}
onLock={() => lockMutation.mutate(field.path ?? "")}
Expand Down Expand Up @@ -461,6 +465,7 @@ interface FieldRowProps {
lastChanged?: { actor: string; time: string };
editing: boolean;
showLocks: boolean;
productMode: boolean;
onChange: (value: string) => void;
onUndo: () => void;
onLock: () => void;
Expand All @@ -475,6 +480,7 @@ function FieldRow({
lastChanged,
editing,
showLocks,
productMode,
onChange,
onUndo,
onLock,
Expand All @@ -494,7 +500,7 @@ function FieldRow({
: "border-gray-200 dark:border-gray-800"
}`}
>
<TypeBadge type={field.type} />
<TypeBadge type={field.type} subtle={productMode} />
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
{field.title ? (
Expand All @@ -505,7 +511,15 @@ function FieldRow({
</span>
</>
) : (
<span className="font-mono text-sm font-medium">{field.path}</span>
<span
className={
productMode
? "font-mono text-sm text-gray-500 dark:text-gray-400"
: "font-mono text-sm font-medium"
}
>
{field.path}
</span>
)}
{isDirty && <span className="h-2 w-2 rounded-full bg-blue-500" title="Modified" />}
{field.nullable && (
Expand All @@ -524,7 +538,15 @@ function FieldRow({
{field.sensitive && <SensitiveBadge />}
</div>
{field.description && (
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">{field.description}</p>
<p
className={
productMode
? "mb-2 text-sm text-gray-600 dark:text-gray-300"
: "mb-2 text-xs text-gray-500 dark:text-gray-400"
}
>
{field.description}
</p>
)}
{field.deprecated && field.redirectTo && (
<p className="mb-2 text-xs text-amber-600 dark:text-amber-400">
Expand Down Expand Up @@ -657,11 +679,14 @@ function formatDuration(raw: string): string {
return parts.join("") || "0s";
}

function TypeBadge({ type }: { type?: FieldType }) {
function TypeBadge({ type, subtle = false }: { type?: FieldType; subtle?: boolean }) {
const colorClass = subtle
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500"
: fieldTypeColor(type);
return (
<span className="group relative pt-1">
<span
className={`inline-flex w-9 items-center justify-center rounded py-0.5 text-xs font-bold leading-tight ${fieldTypeColor(type)}`}
className={`inline-flex w-9 items-center justify-center rounded py-0.5 text-xs font-bold leading-tight ${colorClass}`}
>
{fieldTypeIcon(type)}
</span>
Expand Down
Loading
Loading