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
36 changes: 3 additions & 33 deletions src/pages/orchestration/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
fetchTotalPluginsCount,
replaceTotalPluginsPlaceholder,
} from "~/utils/plugins/pluginCount"
import { resolveBlueprintRenders, fetchToolBlueprintCount } from "~/utils/blueprints/orchestrationBlueprints"

import GorgiasLogo from "~/contents/customer-stories/gorgias-using-declarative-data-engineering-orchestration-with-kestra/logo.svg?raw"
import GraviteeLogo from "~/contents/customer-stories/when-your-api-writes-its-own-docs-with-kestra/logo.svg?raw"
Expand All @@ -49,7 +50,6 @@ import SophtLogo from "~/components/orchestrate/assets/sopht.svg?raw"
import BsiLogo from "~/components/orchestrate/assets/bsi.svg?raw"
import CenshareLogo from "~/components/orchestrate/assets/censhare.svg?raw"
import WestBendLogo from "~/components/orchestrate/assets/west-bend.svg?raw"
import Fortune500Logo from "~/components/enterprise/assets/fortune500.svg?raw"

const versusLogoModules = import.meta.glob<string>(
"../../components/versus/assets/*.svg",
Expand Down Expand Up @@ -87,7 +87,6 @@ const LOGO_MAP: Record<string, string> = {
bsi: optimizeSvgIcon(BsiLogo, "bsi-orch-logo"),
censhare: optimizeSvgIcon(CenshareLogo, "censhare-orch-logo"),
"west-bend": optimizeSvgIcon(WestBendLogo, "west-bend-orch-logo"),
fortune500: optimizeSvgIcon(Fortune500Logo, "fortune500-orch-logo"),
}

export async function getStaticPaths() {
Expand Down Expand Up @@ -166,38 +165,9 @@ const {
resources,
} = replaceTotalPluginsPlaceholder(entry.data, totalPlugins)

const blueprintItems = await Promise.all(
blueprints.items.map(async (item) => {
if (!item.blueprintId) return item
try {
const [bp, graph] = await Promise.all([
$fetchApiCached<{ flow: string }>(
`/blueprints/${item.blueprintId}/versions/latest`,
),
$fetchApiCached(
`/blueprints/${item.blueprintId}/versions/latest/graph`,
),
])
return { ...item, render: { source: bp.flow, graph } }
} catch (e) {
console.warn(
`Could not fetch blueprint "${item.blueprintId}" for ${entry.id}`,
e,
)
return item
}
}),
)
const blueprintItems = await resolveBlueprintRenders(blueprints.items)

let allBlueprintsCount = 0
try {
const search = await $fetchApiCached<{ total: number }>(
`/blueprints/versions/latest?q=${encodeURIComponent(tool.name)}&size=1`,
)
allBlueprintsCount = search?.total ?? 0
} catch (e) {
console.warn(`Could not fetch blueprint count for ${tool.name}`, e)
}
const allBlueprintsCount = await fetchToolBlueprintCount(tool.name)

let pluginIcon = ""
try {
Expand Down
159 changes: 159 additions & 0 deletions src/utils/blueprints/orchestrationBlueprints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
resolveBlueprintRenders,
fetchToolBlueprintCount,
buildBlueprintFlowUrl,
buildBlueprintGraphUrl,
buildToolBlueprintCountUrl,
} from "./orchestrationBlueprints";

vi.mock("~/utils/fetch", () => ({
$fetchApiCachedOptional: vi.fn(),
}));

import { $fetchApiCachedOptional } from "~/utils/fetch";

describe("orchestrationBlueprints", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("buildBlueprintFlowUrl", () => {
it("builds correct flow URL", () => {
expect(buildBlueprintFlowUrl("my-blueprint")).toBe("/blueprints/my-blueprint/versions/latest");
});
});

describe("buildBlueprintGraphUrl", () => {
it("builds correct graph URL", () => {
expect(buildBlueprintGraphUrl("my-blueprint")).toBe("/blueprints/my-blueprint/versions/latest/graph");
});
});

describe("buildToolBlueprintCountUrl", () => {
it("builds correct count URL with encoding", () => {
expect(buildToolBlueprintCountUrl("my tool")).toBe("/blueprints/versions/latest?q=my%20tool&size=1");
});

it("handles special characters", () => {
expect(buildToolBlueprintCountUrl("tool&name")).toBe("/blueprints/versions/latest?q=tool%26name&size=1");
});
});

describe("resolveBlueprintRenders", () => {
it("returns items with render when both flow and graph succeed", async () => {
vi.mocked($fetchApiCachedOptional)
.mockResolvedValueOnce({ flow: "flow source 1" })
.mockResolvedValueOnce({ flow: "flow source 2" })
.mockResolvedValueOnce({ nodes: [], edges: [] })
.mockResolvedValueOnce({ nodes: [1], edges: [2] });

const items = [
{ blueprintId: "bp1", name: "Item 1" },
{ blueprintId: "bp2", name: "Item 2" },
];

const result = await resolveBlueprintRenders(items);

expect(result).toEqual([
{ blueprintId: "bp1", name: "Item 1", render: { source: "flow source 1", graph: { nodes: [], edges: [] } } },
{ blueprintId: "bp2", name: "Item 2", render: { source: "flow source 2", graph: { nodes: [1], edges: [2] } } },
]);
});

it("returns item without render when flow fetch fails", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce(null);

const items = [{ blueprintId: "bp1", name: "Item 1" }];

const result = await resolveBlueprintRenders(items);

expect(result).toEqual([{ blueprintId: "bp1", name: "Item 1" }]);
});

it("returns item with source only when graph fetch fails but flow succeeds", async () => {
vi.mocked($fetchApiCachedOptional)
.mockResolvedValueOnce({ flow: "flow source" })
.mockResolvedValueOnce(null);

const items = [{ blueprintId: "bp1", name: "Item 1" }];

const result = await resolveBlueprintRenders(items);

expect(result).toEqual([
{ blueprintId: "bp1", name: "Item 1", render: { source: "flow source", graph: null } },
]);
});

it("passes through items without blueprintId unchanged", async () => {
const items = [{ name: "No blueprint" }, { blueprintId: "bp1", name: "Has blueprint" }];

vi.mocked($fetchApiCachedOptional)
.mockResolvedValueOnce({ flow: "flow" })
.mockResolvedValueOnce({ nodes: [] });

const result = await resolveBlueprintRenders(items);

expect(result[0]).toEqual({ name: "No blueprint" });
expect(result[1]).toHaveProperty("render");
});

it("handles empty array", async () => {
const result = await resolveBlueprintRenders([]);
expect(result).toEqual([]);
});

it("handles fetch returning null gracefully", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce(null);

const items = [{ blueprintId: "bp1", name: "Item 1" }];

const result = await resolveBlueprintRenders(items);

expect(result).toEqual([{ blueprintId: "bp1", name: "Item 1" }]);
});
});

describe("fetchToolBlueprintCount", () => {
it("returns total on success", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce({ total: 42 });

const result = await fetchToolBlueprintCount("terraform");

expect(result).toBe(42);
expect($fetchApiCachedOptional).toHaveBeenCalledWith("/blueprints/versions/latest?q=terraform&size=1");
});

it("returns 0 when total is missing", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce({});

const result = await fetchToolBlueprintCount("terraform");

expect(result).toBe(0);
});

it("returns 0 on 404", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce(null);

const result = await fetchToolBlueprintCount("terraform");

expect(result).toBe(0);
});

it("returns 0 when fetch returns null", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce(null);

const result = await fetchToolBlueprintCount("terraform");

expect(result).toBe(0);
});

it("encodes tool name in URL", async () => {
vi.mocked($fetchApiCachedOptional).mockResolvedValueOnce({ total: 5 });

await fetchToolBlueprintCount("my tool");

expect($fetchApiCachedOptional).toHaveBeenCalledWith("/blueprints/versions/latest?q=my%20tool&size=1");
});
});
});
56 changes: 56 additions & 0 deletions src/utils/blueprints/orchestrationBlueprints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { $fetchApiCachedOptional } from "~/utils/fetch";

export interface BlueprintRender {
source: string;
graph: unknown | null;
}

export async function resolveBlueprintRenders<T extends { blueprintId?: string }>(
items: T[]
): Promise<(T & { render?: BlueprintRender })[]> {
return Promise.all(
items.map(async (item) => {
if (!item.blueprintId) {
return item;
}

const flow = await $fetchApiCachedOptional<{ flow: string }>(
buildBlueprintFlowUrl(item.blueprintId)
);

if (!flow) {
return item;
}

const graph = await $fetchApiCachedOptional<unknown>(
buildBlueprintGraphUrl(item.blueprintId)
);

return {
...item,
render: {
source: flow.flow,
graph: graph ?? null,
},
};
})
);
}

export function buildBlueprintFlowUrl(blueprintId: string): string {
return `/blueprints/${blueprintId}/versions/latest`;
}

export function buildBlueprintGraphUrl(blueprintId: string): string {
return `/blueprints/${blueprintId}/versions/latest/graph`;
}

export function buildToolBlueprintCountUrl(toolName: string): string {
return `/blueprints/versions/latest?q=${encodeURIComponent(toolName)}&size=1`;
}

export async function fetchToolBlueprintCount(toolName: string): Promise<number> {
const url = buildToolBlueprintCountUrl(toolName);
const result = await $fetchApiCachedOptional<{ total: number }>(url);
return result?.total ?? 0;
}