@shortlink-org/portolan 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { djangoAggregateCandidates, djangoAggregateMessage } from "../../lib/django-aggregates";
|
|
4
|
+
import { djangoAggregateProposals, saveDjangoAggregates } from "../../lib/local-api";
|
|
5
|
+
import { localKeys, localStatusQuery } from "../../lib/queries";
|
|
6
|
+
import type { WarningDiagnostic } from "../../lib/warnings";
|
|
7
|
+
import { useToastStore } from "../../app/toast";
|
|
8
|
+
|
|
9
|
+
const CANDIDATE_KEY = ["local", "django-aggregates"];
|
|
10
|
+
|
|
11
|
+
export function DjangoAggregateChoices({ warnings }: { warnings: WarningDiagnostic[] }) {
|
|
12
|
+
const status = useQuery(localStatusQuery());
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
const say = useToastStore((state) => state.say);
|
|
15
|
+
const proposals = useQuery({
|
|
16
|
+
queryKey: [...CANDIDATE_KEY, status.data?.setup.run?.finishedAt],
|
|
17
|
+
queryFn: djangoAggregateProposals,
|
|
18
|
+
enabled: status.isSuccess,
|
|
19
|
+
retry: false,
|
|
20
|
+
});
|
|
21
|
+
const [choices, setChoices] = useState<Record<string, string>>({});
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
const [error, setError] = useState("");
|
|
24
|
+
const [saved, setSaved] = useState(false);
|
|
25
|
+
const rows = warnings.flatMap((warning) => {
|
|
26
|
+
const candidates = warning.aggregateCandidates ?? djangoAggregateCandidates(warning.message);
|
|
27
|
+
if (!candidates) return [];
|
|
28
|
+
const matches = proposals.data?.proposals.filter((proposal) => proposal.plugin === warning.plugin && proposal.app === candidates.app
|
|
29
|
+
&& djangoAggregateMessage(proposal.message).slice(0, 500) === djangoAggregateMessage(warning.message).slice(0, 500)) ?? [];
|
|
30
|
+
return [{ ...candidates, key: `${warning.plugin}:${warning.message}`, proposal: matches.length === 1 ? matches[0] : undefined }];
|
|
31
|
+
});
|
|
32
|
+
const selections = rows.flatMap((row) => row.proposal && choices[row.key] ? [{ id: row.proposal.id, model: choices[row.key]! }] : []);
|
|
33
|
+
const canSave = Boolean(proposals.data && !proposals.data.stale && !status.data?.activeRun && selections.length && !busy);
|
|
34
|
+
|
|
35
|
+
async function save() {
|
|
36
|
+
if (!canSave || !proposals.data) return;
|
|
37
|
+
setBusy(true); setError(""); setSaved(false);
|
|
38
|
+
try {
|
|
39
|
+
await saveDjangoAggregates(proposals.data.revision, selections);
|
|
40
|
+
setSaved(true);
|
|
41
|
+
// Refreshing the manifest hides the now-stale report and unmounts this
|
|
42
|
+
// picker; keep the next step visible outside that report.
|
|
43
|
+
say("Aggregate roots saved to portolan.json. Preview generated diff to update the catalog.");
|
|
44
|
+
await Promise.all([
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: CANDIDATE_KEY }),
|
|
46
|
+
queryClient.invalidateQueries({ queryKey: localKeys.status }),
|
|
47
|
+
]);
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
50
|
+
} finally { setBusy(false); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return <div className="mt-3 space-y-3">
|
|
54
|
+
<p className="text-muted">Choose the model that owns each application’s aggregate. Other concrete models become its entities.</p>
|
|
55
|
+
{rows.map((row) => <div key={row.key} className="rounded-control border border-line p-3">
|
|
56
|
+
<label className="block font-medium text-ink">
|
|
57
|
+
Aggregate root for <span className="mono">{row.app}</span>
|
|
58
|
+
<select aria-label={`Aggregate root for ${row.app}`} className="mt-2 block w-full rounded-control border border-line bg-canvas px-3 py-2 text-ink" value={choices[row.key] ?? ""} onChange={(event) => { setChoices({ ...choices, [row.key]: event.target.value }); setSaved(false); }} disabled={busy}>
|
|
59
|
+
<option value="">Choose a model…</option>
|
|
60
|
+
{row.models.map((model) => <option key={model.name} value={model.name}>{model.name}</option>)}
|
|
61
|
+
</select>
|
|
62
|
+
</label>
|
|
63
|
+
<details className="mt-2 text-muted">
|
|
64
|
+
<summary className="cursor-pointer">{row.models.length} candidates · source locations</summary>
|
|
65
|
+
<ul className="mono mt-2 space-y-1">{row.models.map((model) => <li key={model.name} className="break-words">{model.name} · {model.path}:{model.line}</li>)}</ul>
|
|
66
|
+
</details>
|
|
67
|
+
{choices[row.key] ? <pre className="mono mt-2 overflow-auto rounded-control bg-canvas p-2 text-ink">{JSON.stringify({ aggregates: { [row.app]: choices[row.key] } }, null, 2)}</pre> : null}
|
|
68
|
+
</div>)}
|
|
69
|
+
{status.isSuccess ? <>
|
|
70
|
+
<button className="rounded-control border border-line bg-canvas px-3 py-2 font-medium text-accent hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50" onClick={() => void save()} disabled={!canSave}>{busy ? "Saving…" : "Save selected roots"}</button>
|
|
71
|
+
<p className="text-muted">Saves to the matching extraction steps in portolan.json. Regenerate afterward to update the catalog and warnings.</p>
|
|
72
|
+
{proposals.data?.stale && !saved ? <p className="text-declared">Regenerate before saving: these candidates belong to an older manifest.</p> : null}
|
|
73
|
+
{proposals.isError ? <p role="alert" className="text-unresolved">Could not load the saved extraction report. Regenerate and reopen Settings.</p> : null}
|
|
74
|
+
{rows.some((row) => !row.proposal) && proposals.isSuccess ? <p className="text-muted">Candidates from a trial can be saved after applying the project and generating its catalog.</p> : null}
|
|
75
|
+
</> : <p className="text-muted">To save, open Portolan locally or copy the selected aggregates option into the matching extraction step in portolan.json.</p>}
|
|
76
|
+
{saved ? <p role="status" className="text-verified">Aggregate roots saved. Regenerate to apply the choices to the catalog.</p> : null}
|
|
77
|
+
{error ? <p role="alert" className="text-unresolved">{error}</p> : null}
|
|
78
|
+
</div>;
|
|
79
|
+
}
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import { ExternalLink, Save, Trash2 } from "lucide-react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import type { StoreApi, UseBoundStore } from "zustand";
|
|
3
4
|
import { SectionTitle } from "../../components/PageHeader";
|
|
4
5
|
import { TechIcon } from "../../components/TechIcon";
|
|
5
|
-
import {
|
|
6
|
+
import { useConfluence } from "../../lib/confluence";
|
|
7
|
+
import { normalizeIntegrationUrl } from "../../lib/integration-url";
|
|
8
|
+
import type { IntegrationState } from "../../lib/integration-url";
|
|
9
|
+
import { useKafkaUi } from "../../lib/kafka-ui";
|
|
10
|
+
import { useNotion } from "../../lib/notion";
|
|
6
11
|
import { techGlyph } from "../../lib/tech";
|
|
7
12
|
|
|
8
13
|
const FIELD =
|
|
9
14
|
"mono w-full rounded-control border border-line bg-canvas px-3 py-2 text-ink outline-none focus:border-accent";
|
|
10
15
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
function IntegrationCard({
|
|
17
|
+
name,
|
|
18
|
+
brand,
|
|
19
|
+
summary,
|
|
20
|
+
label,
|
|
21
|
+
placeholder,
|
|
22
|
+
hint,
|
|
23
|
+
store,
|
|
24
|
+
}: {
|
|
25
|
+
/** The heading, and what the open button says. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** The brand whose mark sits beside the heading. */
|
|
28
|
+
brand: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
label: string;
|
|
31
|
+
placeholder: string;
|
|
32
|
+
/** What a valid URL means: shown under the field. */
|
|
33
|
+
hint: string;
|
|
34
|
+
store: UseBoundStore<StoreApi<IntegrationState>>;
|
|
35
|
+
}) {
|
|
36
|
+
const configured = store((state) => state.url);
|
|
37
|
+
const setUrl = store((state) => state.setUrl);
|
|
14
38
|
const [draft, setDraft] = useState(configured);
|
|
15
|
-
const normalized =
|
|
39
|
+
const normalized = normalizeIntegrationUrl(draft);
|
|
16
40
|
const valid = normalized !== null;
|
|
17
41
|
const dirty = valid && normalized !== configured;
|
|
18
|
-
const glyph = techGlyph(
|
|
42
|
+
const glyph = techGlyph(brand);
|
|
19
43
|
|
|
20
44
|
const save = () => {
|
|
21
45
|
if (normalized === null) return;
|
|
@@ -36,19 +60,17 @@ function KafkaUiCard() {
|
|
|
36
60
|
</span>
|
|
37
61
|
<div className="min-w-0 flex-1">
|
|
38
62
|
<div className="flex flex-wrap items-center gap-2">
|
|
39
|
-
<h2 className="font-semibold text-ink">
|
|
63
|
+
<h2 className="font-semibold text-ink">{name}</h2>
|
|
40
64
|
<span className={`chip ${configured ? "status-verified" : "text-muted"}`}>
|
|
41
65
|
{configured ? "configured" : "not configured"}
|
|
42
66
|
</span>
|
|
43
67
|
</div>
|
|
44
|
-
<p className="mt-1 text-muted">
|
|
45
|
-
Opens catalogued Kafka topics in your Kafbat Kafka UI installation.
|
|
46
|
-
</p>
|
|
68
|
+
<p className="mt-1 text-muted">{summary}</p>
|
|
47
69
|
</div>
|
|
48
70
|
</div>
|
|
49
71
|
|
|
50
72
|
<label className="mt-4 block">
|
|
51
|
-
<span className="label mb-1.5 block">
|
|
73
|
+
<span className="label mb-1.5 block">{label}</span>
|
|
52
74
|
<input
|
|
53
75
|
type="url"
|
|
54
76
|
value={draft}
|
|
@@ -56,7 +78,7 @@ function KafkaUiCard() {
|
|
|
56
78
|
onKeyDown={(event) => {
|
|
57
79
|
if (event.key === "Enter" && dirty) save();
|
|
58
80
|
}}
|
|
59
|
-
placeholder=
|
|
81
|
+
placeholder={placeholder}
|
|
60
82
|
spellCheck={false}
|
|
61
83
|
autoComplete="off"
|
|
62
84
|
aria-invalid={!valid}
|
|
@@ -64,9 +86,7 @@ function KafkaUiCard() {
|
|
|
64
86
|
/>
|
|
65
87
|
</label>
|
|
66
88
|
<p className={`mono mt-2 ${valid ? "text-muted" : "text-unresolved"}`}>
|
|
67
|
-
{valid
|
|
68
|
-
? "Use a cluster or topics URL to open the exact topic. A plain installation URL opens Kafka UI itself."
|
|
69
|
-
: "Enter an http:// or https:// URL."}
|
|
89
|
+
{valid ? hint : "Enter an http:// or https:// URL."}
|
|
70
90
|
</p>
|
|
71
91
|
|
|
72
92
|
<div className="mt-4 flex flex-wrap items-center gap-2 border-t border-line pt-4">
|
|
@@ -87,7 +107,7 @@ function KafkaUiCard() {
|
|
|
87
107
|
rel="noreferrer"
|
|
88
108
|
className="tbtn px-3 py-1.5 text-ink"
|
|
89
109
|
>
|
|
90
|
-
Open
|
|
110
|
+
Open {name}
|
|
91
111
|
<ExternalLink size={14} aria-hidden />
|
|
92
112
|
</a>
|
|
93
113
|
<button
|
|
@@ -110,7 +130,33 @@ export function IntegrationsSettings() {
|
|
|
110
130
|
<section>
|
|
111
131
|
<SectionTitle right="stored in this browser">Integrations</SectionTitle>
|
|
112
132
|
<div className="grid gap-grid lg:grid-cols-2">
|
|
113
|
-
<
|
|
133
|
+
<IntegrationCard
|
|
134
|
+
name="Kafka UI"
|
|
135
|
+
brand="Kafka"
|
|
136
|
+
summary="Opens catalogued Kafka topics in your Kafbat Kafka UI installation."
|
|
137
|
+
label="Kafka UI cluster URL"
|
|
138
|
+
placeholder="https://kafka.example/ui/clusters/production"
|
|
139
|
+
hint="Use a cluster or topics URL to open the exact topic. A plain installation URL opens Kafka UI itself."
|
|
140
|
+
store={useKafkaUi}
|
|
141
|
+
/>
|
|
142
|
+
<IntegrationCard
|
|
143
|
+
name="Confluence"
|
|
144
|
+
brand="Confluence"
|
|
145
|
+
summary="Searches your wiki for a service or context by name, from its page."
|
|
146
|
+
label="Confluence site or space URL"
|
|
147
|
+
placeholder="https://acme.atlassian.net/wiki/spaces/ARCH"
|
|
148
|
+
hint="A space URL searches that space only. A site URL searches the whole wiki. Cloud and Server are both understood."
|
|
149
|
+
store={useConfluence}
|
|
150
|
+
/>
|
|
151
|
+
<IntegrationCard
|
|
152
|
+
name="Notion"
|
|
153
|
+
brand="Notion"
|
|
154
|
+
summary="Opens the Notion workspace where the documentation lives, from a service or context page."
|
|
155
|
+
label="Notion workspace or teamspace URL"
|
|
156
|
+
placeholder="https://www.notion.so/acme"
|
|
157
|
+
hint="Notion has no search address, so every page opens this URL as it is."
|
|
158
|
+
store={useNotion}
|
|
159
|
+
/>
|
|
114
160
|
</div>
|
|
115
161
|
</section>
|
|
116
162
|
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Recordings, for the whole estate: what the catalog was verified against,
|
|
2
|
+
// and a place to add one without first finding the flow it shows.
|
|
3
|
+
//
|
|
4
|
+
// A recording added here goes to the project the reader names. The trial
|
|
5
|
+
// then says which flows it showed - which is how a reader with an export
|
|
6
|
+
// from the collector and no idea which flow it is finds out.
|
|
7
|
+
|
|
8
|
+
import { useMemo, useRef, useState } from "react";
|
|
9
|
+
import { Disc3, Upload } from "lucide-react";
|
|
10
|
+
import { Link } from "react-router";
|
|
11
|
+
import { catalog } from "../../data";
|
|
12
|
+
import { useToastStore } from "../../app/toast";
|
|
13
|
+
import { TraceTrialPanel } from "../../flow/TraceTrial";
|
|
14
|
+
import { forgetTraceTrial, recallTraceTrial, rememberTraceTrial, sessionStore } from "../../flow/trace-trial-resume";
|
|
15
|
+
import { plural } from "../../lib/format";
|
|
16
|
+
import { startTraceTrial } from "../../lib/local-api";
|
|
17
|
+
import type { SetupProject } from "../../lib/setup-info";
|
|
18
|
+
import { paths } from "../../routes";
|
|
19
|
+
|
|
20
|
+
const FIELD = "w-full rounded-control border border-line bg-canvas px-2.5 py-1.5 text-ink";
|
|
21
|
+
const SCOPE = "settings";
|
|
22
|
+
|
|
23
|
+
/** Every recording the catalog names, with the flows it is an example of. */
|
|
24
|
+
function recordingsInCatalog() {
|
|
25
|
+
const byFile = new Map<string, { traces: Set<string>; flows: Map<string, string> }>();
|
|
26
|
+
for (const flow of catalog.flows) {
|
|
27
|
+
for (const example of flow.examples ?? []) {
|
|
28
|
+
const entry = byFile.get(example.recording) ?? { traces: new Set(), flows: new Map() };
|
|
29
|
+
entry.traces.add(example.traceId);
|
|
30
|
+
entry.flows.set(flow.slug, flow.name);
|
|
31
|
+
byFile.set(example.recording, entry);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...byFile.entries()]
|
|
35
|
+
.map(([recording, entry]) => ({ recording, traces: entry.traces.size, flows: [...entry.flows.entries()] }))
|
|
36
|
+
.sort((a, b) => a.recording.localeCompare(b.recording));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function RecordingSettings({ local, projects }: { local: boolean; projects: readonly SetupProject[] }) {
|
|
40
|
+
const say = useToastStore((s) => s.say);
|
|
41
|
+
const input = useRef<HTMLInputElement | null>(null);
|
|
42
|
+
const [projectId, setProjectId] = useState(projects[0]?.id ?? "");
|
|
43
|
+
// Picked back up from before a reload: keeping a recording reloads the
|
|
44
|
+
// page twice, and the panel outlives it.
|
|
45
|
+
const [resumed] = useState(() => recallTraceTrial(sessionStore(), SCOPE));
|
|
46
|
+
const [runId, setRunId] = useState<string | null>(resumed?.runId ?? null);
|
|
47
|
+
const [busy, setBusy] = useState(false);
|
|
48
|
+
const known = useMemo(recordingsInCatalog, []);
|
|
49
|
+
|
|
50
|
+
async function upload(file: File) {
|
|
51
|
+
if (!projectId) return;
|
|
52
|
+
setBusy(true);
|
|
53
|
+
try {
|
|
54
|
+
const started = await startTraceTrial(file, projectId);
|
|
55
|
+
rememberTraceTrial(sessionStore(), SCOPE, { runId: started.runId, writeRunId: null });
|
|
56
|
+
setRunId(started.runId);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
say(cause instanceof Error ? cause.message : String(cause));
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
if (input.current) input.current.value = "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col gap-5">
|
|
67
|
+
<div className="rounded-card border border-line p-card shadow-xs">
|
|
68
|
+
<div className="flex items-center gap-2 font-semibold text-ink">
|
|
69
|
+
<Disc3 size={16} className="text-accent" /> Add a recording
|
|
70
|
+
</div>
|
|
71
|
+
<p className="mt-1 max-w-prose text-muted">
|
|
72
|
+
An OTLP JSON export - one batch per file or one per line, as a collector's file exporter writes it. The generator runs over a copy of the workspace with it in place and shows which flows it verified, what it added to them and which names it could not place, before the recording is kept beside the project.
|
|
73
|
+
</p>
|
|
74
|
+
{local ? (
|
|
75
|
+
<div className="mt-3 flex flex-wrap items-end gap-3">
|
|
76
|
+
<label className="min-w-56">
|
|
77
|
+
<span className="label mb-1.5 block">project</span>
|
|
78
|
+
<select className={FIELD} value={projectId} onChange={(e) => setProjectId(e.target.value)}>
|
|
79
|
+
{projects.length ? null : <option value="">no projects in portolan.json</option>}
|
|
80
|
+
{projects.map((p) => <option key={p.id} value={p.id}>{p.name} · {p.root}</option>)}
|
|
81
|
+
</select>
|
|
82
|
+
</label>
|
|
83
|
+
<button type="button" className="product-primary" disabled={busy || !projectId || runId !== null} onClick={() => input.current?.click()}>
|
|
84
|
+
<Upload size={14} /> Choose a file
|
|
85
|
+
</button>
|
|
86
|
+
<input
|
|
87
|
+
ref={input}
|
|
88
|
+
type="file"
|
|
89
|
+
accept=".jsonl,.json,.ndjson,application/json"
|
|
90
|
+
className="hidden"
|
|
91
|
+
onChange={(e) => { const file = e.target.files?.[0]; if (file) void upload(file); }}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<div className="empty mt-3">
|
|
96
|
+
Start <code>portolan dev</code> to add recordings in this repository.
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
{runId ? (
|
|
100
|
+
<div className="mt-4 border-t border-line pt-4">
|
|
101
|
+
<TraceTrialPanel
|
|
102
|
+
key={runId}
|
|
103
|
+
runId={runId}
|
|
104
|
+
initialWriteRunId={resumed?.runId === runId ? resumed.writeRunId : null}
|
|
105
|
+
onWriteStarted={(writeRunId) => rememberTraceTrial(sessionStore(), SCOPE, { runId, writeRunId })}
|
|
106
|
+
onDone={() => { forgetTraceTrial(sessionStore()); setRunId(null); }}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<section>
|
|
113
|
+
<div className="label mb-2">recordings the catalog was verified against · {known.length}</div>
|
|
114
|
+
{known.length ? (
|
|
115
|
+
<div className="divide-y divide-line rounded-control border border-line">
|
|
116
|
+
{known.map((entry) => (
|
|
117
|
+
<div key={entry.recording} className="grid gap-1 px-3 py-2 sm:grid-cols-[1fr_auto]">
|
|
118
|
+
<div className="min-w-0">
|
|
119
|
+
<div className="mono truncate text-ink" title={entry.recording}>{entry.recording}</div>
|
|
120
|
+
<div className="mt-0.5 flex flex-wrap gap-x-2 text-muted">
|
|
121
|
+
{entry.flows.map(([slug, name]) => (
|
|
122
|
+
<Link key={slug} to={paths.flow(slug)} className="hover:underline">{name}</Link>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<span className="mono text-muted sm:text-right">{entry.traces} {plural(entry.traces, "trace")} kept as {plural(entry.traces, "an example", "examples")}</span>
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="rounded-control border border-line bg-surface px-3 py-3 text-muted">
|
|
132
|
+
No flow in this catalog carries a recording yet. A verify step with the otel plugin reads them; adding one above writes that step for you.
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</section>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|