@shortlink-org/portolan 0.1.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/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -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 +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.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-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
|
|
3
|
+
import { TREE_KEYS, treeKey } from "./tree-keys";
|
|
4
|
+
import type { TreeRow } from "./tree-keys";
|
|
5
|
+
import { Link, NavLink, useLocation, useMatch } from "react-router";
|
|
6
|
+
import {
|
|
7
|
+
DURATION,
|
|
8
|
+
LayoutGroup,
|
|
9
|
+
Unfold,
|
|
10
|
+
m,
|
|
11
|
+
transitions,
|
|
12
|
+
} from "../lib/motion";
|
|
13
|
+
import {
|
|
14
|
+
Check,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
GripVertical,
|
|
17
|
+
PanelLeftOpen,
|
|
18
|
+
PinOff,
|
|
19
|
+
Settings2,
|
|
20
|
+
TriangleAlert,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { catalog, index } from "../data";
|
|
23
|
+
import type {
|
|
24
|
+
Aggregate,
|
|
25
|
+
Block,
|
|
26
|
+
BoundedContext,
|
|
27
|
+
Classification,
|
|
28
|
+
Event,
|
|
29
|
+
Operation,
|
|
30
|
+
RpcMethod,
|
|
31
|
+
RpcService,
|
|
32
|
+
Service,
|
|
33
|
+
Store,
|
|
34
|
+
Table,
|
|
35
|
+
View,
|
|
36
|
+
Enum,
|
|
37
|
+
} from "../catalog";
|
|
38
|
+
import { allModules, allTerms, enumsOf, storeViews } from "../catalog";
|
|
39
|
+
import { adrNumber, newestAccepted } from "../lib/adr";
|
|
40
|
+
import { contextName, ctxStyle } from "../lib/context-color";
|
|
41
|
+
import { contextStats, problems } from "../lib/derive";
|
|
42
|
+
import { dataProblems } from "../lib/data-problems";
|
|
43
|
+
import { protoProblems } from "../lib/proto-problems";
|
|
44
|
+
import { vocabularies } from "../language/cards";
|
|
45
|
+
import {
|
|
46
|
+
FLOW_HEALTH_NOTE,
|
|
47
|
+
groupFlowsByOwner,
|
|
48
|
+
reachDots,
|
|
49
|
+
visibleEntries,
|
|
50
|
+
} from "../lib/flow-tree";
|
|
51
|
+
import type { FlowEntry, FlowHealth, FlowGroup } from "../lib/flow-tree";
|
|
52
|
+
import type { Kind, LeafKind } from "../lib/kinds";
|
|
53
|
+
import { plural } from "../lib/format";
|
|
54
|
+
import {
|
|
55
|
+
FLOW_GROUPS_KEY,
|
|
56
|
+
SECTIONS_KEY,
|
|
57
|
+
readFlags,
|
|
58
|
+
writeFlags,
|
|
59
|
+
} from "../lib/sidebar-prefs";
|
|
60
|
+
import type { Flags } from "../lib/sidebar-prefs";
|
|
61
|
+
import { KindIcon } from "../components/kind";
|
|
62
|
+
import { CompassRose, Wordmark } from "../components/logo";
|
|
63
|
+
import { isStruck } from "../components/primitives";
|
|
64
|
+
import { packageAnchor, paths } from "../routes";
|
|
65
|
+
import { selectionHash } from "../selection/hash";
|
|
66
|
+
import { STORE_KIND_LABEL, StoreKindMark } from "../er/StoreHeader";
|
|
67
|
+
import { resolvePin, usePinsStore } from "./pins";
|
|
68
|
+
import { resolveSelection, selectionFor } from "../selection/model";
|
|
69
|
+
import { selectsInPlace } from "../selection/pages";
|
|
70
|
+
import { useSelectionStore } from "../selection/store";
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// The rows under a service, grouped the way the tree draws them. The tree
|
|
74
|
+
// shows everything: searching the catalog is ⌘K's job, and a second box that
|
|
75
|
+
// narrowed this pane was the same question asked twice on one screen.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
interface AggregateRows {
|
|
79
|
+
aggregate: Aggregate;
|
|
80
|
+
valueObjects: Block[];
|
|
81
|
+
entities: Block[];
|
|
82
|
+
enums: Enum[];
|
|
83
|
+
events: Event[];
|
|
84
|
+
commands: Operation[];
|
|
85
|
+
queries: Operation[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface StoreRows {
|
|
89
|
+
store: Store;
|
|
90
|
+
tables: Table[];
|
|
91
|
+
views: View[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ServiceRows {
|
|
95
|
+
service: Service;
|
|
96
|
+
aggregates: AggregateRows[];
|
|
97
|
+
endpoints: EndpointRows[];
|
|
98
|
+
stores: StoreRows[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface EndpointRows {
|
|
102
|
+
provided: RpcService;
|
|
103
|
+
methods: RpcMethod[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What the service answers, one entry per interface it declares. */
|
|
107
|
+
function endpointRows(service: Service): EndpointRows[] {
|
|
108
|
+
return service.provides.map((provided) => ({
|
|
109
|
+
provided,
|
|
110
|
+
methods: provided.methods,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The stores a service owns. Read-only stores are left out: the tree is a map
|
|
116
|
+
* of what each service is responsible for, and a store it borrows belongs
|
|
117
|
+
* under its owner.
|
|
118
|
+
*/
|
|
119
|
+
function storeRows(service: Service): StoreRows[] {
|
|
120
|
+
return (catalog.stores ?? [])
|
|
121
|
+
.filter((store) => store.owner === service.id)
|
|
122
|
+
.map((store) => ({
|
|
123
|
+
store,
|
|
124
|
+
tables: store.tables,
|
|
125
|
+
views: storeViews(store),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function aggregateRows(aggregate: Aggregate): AggregateRows {
|
|
130
|
+
const ops = (kind: "command" | "query"): Operation[] =>
|
|
131
|
+
aggregate.operations.filter((o) => o.kind === kind);
|
|
132
|
+
return {
|
|
133
|
+
aggregate,
|
|
134
|
+
valueObjects: aggregate.valueObjects,
|
|
135
|
+
entities: aggregate.entities,
|
|
136
|
+
enums: enumsOf(aggregate),
|
|
137
|
+
events: aggregate.events,
|
|
138
|
+
commands: ops("command"),
|
|
139
|
+
queries: ops("query"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function contextRows(context: BoundedContext): {
|
|
144
|
+
context: BoundedContext;
|
|
145
|
+
services: ServiceRows[];
|
|
146
|
+
} {
|
|
147
|
+
return {
|
|
148
|
+
context,
|
|
149
|
+
services: context.services.map((service) => ({
|
|
150
|
+
service,
|
|
151
|
+
aggregates: service.aggregates.map(aggregateRows),
|
|
152
|
+
endpoints: endpointRows(service),
|
|
153
|
+
stores: storeRows(service),
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Contexts, in the order the reader should meet them: where the estate
|
|
160
|
+
* competes first, what holds it up next, what it bought last, and what it has
|
|
161
|
+
* not yet rated at the end. This is the ONE place classification orders
|
|
162
|
+
* anything - everywhere else it is a badge and nothing more.
|
|
163
|
+
*/
|
|
164
|
+
const CLASSIFICATION_ORDER: Record<Classification, number> = {
|
|
165
|
+
core: 0,
|
|
166
|
+
supporting: 1,
|
|
167
|
+
generic: 2,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function classificationRank(c: Classification | undefined): number {
|
|
171
|
+
return c === undefined ? 3 : CLASSIFICATION_ORDER[c];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const CLASSIFICATION_NOTE: Record<Classification, string> = {
|
|
175
|
+
core: "core domain - where this estate competes",
|
|
176
|
+
supporting: "supporting domain - needed here, but not a differentiator",
|
|
177
|
+
generic: "generic domain - a solved problem, bought or borrowed",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Rows
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
const indent = (depth: number) => 8 + depth * 12;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The selection's light: the raised background and the accent edge of the
|
|
188
|
+
* row that is selected. One element for the whole tree - a new selection is
|
|
189
|
+
* the same light arriving on another row, and it travels there on the settle
|
|
190
|
+
* spring rather than going out here and coming on over there. `-left-0.5`
|
|
191
|
+
* puts its edge over the row's own transparent 2px border, so the text does
|
|
192
|
+
* not move.
|
|
193
|
+
*/
|
|
194
|
+
function SelectionLight() {
|
|
195
|
+
return (
|
|
196
|
+
<m.span
|
|
197
|
+
layoutId="selection"
|
|
198
|
+
aria-hidden
|
|
199
|
+
className="pointer-events-none absolute inset-y-0 right-0 -left-0.5 -z-10 border-l-2 border-accent"
|
|
200
|
+
style={{ background: "var(--surface-2)" }}
|
|
201
|
+
transition={transitions.settle}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Chevron({ open }: { open: boolean }) {
|
|
207
|
+
return (
|
|
208
|
+
<ChevronRight
|
|
209
|
+
size={14}
|
|
210
|
+
aria-hidden
|
|
211
|
+
className="block shrink-0 t-micro transition-transform"
|
|
212
|
+
style={{
|
|
213
|
+
transform: open ? "rotate(90deg)" : "none",
|
|
214
|
+
color: "var(--fg-muted)",
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clicking a row always selects. Whether it also navigates is the question the
|
|
222
|
+
* page on screen answers: on a flow page, picking something that flow already
|
|
223
|
+
* draws is a question about that sequence, so the reader stays put.
|
|
224
|
+
*
|
|
225
|
+
* Only the in-place case writes the store here. A row that navigates carries
|
|
226
|
+
* its selection in the link's hash instead (see `rowTarget`): writing the
|
|
227
|
+
* store first and then letting the link go would race SelectionSync, whose
|
|
228
|
+
* `replace` lands with the pathname it rendered with - the page being left -
|
|
229
|
+
* and undoes the navigation. A selection in the URL wins on arrival, so one
|
|
230
|
+
* write does both jobs. The palette works the same way.
|
|
231
|
+
*/
|
|
232
|
+
function useRowClick(selId: string | undefined) {
|
|
233
|
+
const select = useSelectionStore((s) => s.select);
|
|
234
|
+
const { pathname } = useLocation();
|
|
235
|
+
return (e: React.MouseEvent) => {
|
|
236
|
+
if (selId === undefined) return;
|
|
237
|
+
if (!selectsInPlace(pathname, selectionFor(selId))) return;
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
select(selId, "sidebar");
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Where a row's link goes: its page, with the row's selection in the hash. */
|
|
244
|
+
function rowTarget(to: string, selId: string | undefined): string {
|
|
245
|
+
if (selId === undefined || to.includes("#")) return to;
|
|
246
|
+
return `${to}${selectionHash(selectionFor(selId))}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A leaf: the whole row navigates. Rows that stand for a selectable entity also
|
|
251
|
+
* carry `selId`, which makes them both a writer of the selection and a reader
|
|
252
|
+
* of it - the row lights up whether it was clicked here, on a diagram, or in
|
|
253
|
+
* the palette.
|
|
254
|
+
*/
|
|
255
|
+
function Leaf({
|
|
256
|
+
to,
|
|
257
|
+
depth,
|
|
258
|
+
children,
|
|
259
|
+
title,
|
|
260
|
+
selId,
|
|
261
|
+
}: {
|
|
262
|
+
to: string;
|
|
263
|
+
depth: number;
|
|
264
|
+
children: React.ReactNode;
|
|
265
|
+
title?: string;
|
|
266
|
+
selId?: string;
|
|
267
|
+
}) {
|
|
268
|
+
const selected = useSelectionStore(
|
|
269
|
+
(s) => selId !== undefined && s.selection?.id === selId,
|
|
270
|
+
);
|
|
271
|
+
const onClick = useRowClick(selId);
|
|
272
|
+
return (
|
|
273
|
+
<NavLink
|
|
274
|
+
to={rowTarget(to, selId)}
|
|
275
|
+
end
|
|
276
|
+
title={title}
|
|
277
|
+
data-sel={selId}
|
|
278
|
+
data-tree-row
|
|
279
|
+
data-tree-depth={depth}
|
|
280
|
+
onClick={onClick}
|
|
281
|
+
style={({ isActive }) => ({
|
|
282
|
+
paddingLeft: indent(depth),
|
|
283
|
+
background: isActive && !selected ? "var(--surface-2)" : undefined,
|
|
284
|
+
borderLeftWidth: 2,
|
|
285
|
+
borderLeftStyle: "solid",
|
|
286
|
+
borderLeftColor:
|
|
287
|
+
isActive && !selected ? "var(--accent)" : "transparent",
|
|
288
|
+
})}
|
|
289
|
+
/* The edge is always 2px, transparent when idle: lighting a row must not
|
|
290
|
+
shift the text beside it. The route's own row paints its light in
|
|
291
|
+
place; the SELECTION's light is one element that slides from the row
|
|
292
|
+
it was on to this one. */
|
|
293
|
+
data-nav-item
|
|
294
|
+
className="tree-row relative isolate flex items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
|
|
295
|
+
>
|
|
296
|
+
{selected ? <SelectionLight /> : null}
|
|
297
|
+
{children}
|
|
298
|
+
</NavLink>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A branch: a disclosure triangle that only expands, next to a link that only
|
|
304
|
+
* navigates. Keeping them separate means neither is nested inside the other.
|
|
305
|
+
*
|
|
306
|
+
* `end` is for controls that must be their own target - the unresolved-edge
|
|
307
|
+
* count on a context, which goes somewhere else entirely. It sits OUTSIDE the
|
|
308
|
+
* link, because an anchor may not contain another one, and the link gives up
|
|
309
|
+
* its claim on the rest of the row to make room for it.
|
|
310
|
+
*/
|
|
311
|
+
function Branch({
|
|
312
|
+
to,
|
|
313
|
+
depth,
|
|
314
|
+
open,
|
|
315
|
+
onToggle,
|
|
316
|
+
label,
|
|
317
|
+
children,
|
|
318
|
+
right,
|
|
319
|
+
end,
|
|
320
|
+
under,
|
|
321
|
+
selId,
|
|
322
|
+
}: {
|
|
323
|
+
to: string;
|
|
324
|
+
depth: number;
|
|
325
|
+
open: boolean;
|
|
326
|
+
onToggle: () => void;
|
|
327
|
+
label: string;
|
|
328
|
+
children: React.ReactNode;
|
|
329
|
+
right?: React.ReactNode;
|
|
330
|
+
end?: React.ReactNode;
|
|
331
|
+
under?: React.ReactNode;
|
|
332
|
+
selId?: string;
|
|
333
|
+
}) {
|
|
334
|
+
const selected = useSelectionStore(
|
|
335
|
+
(s) => selId !== undefined && s.selection?.id === selId,
|
|
336
|
+
);
|
|
337
|
+
const onClick = useRowClick(selId);
|
|
338
|
+
const active = useMatch({ path: to, end: true }) !== null && !selected;
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<div
|
|
342
|
+
className="tree-row group relative isolate flex items-stretch t-micro transition-colors hover:bg-surface"
|
|
343
|
+
style={{
|
|
344
|
+
paddingLeft: indent(depth),
|
|
345
|
+
background: active ? "var(--surface-2)" : undefined,
|
|
346
|
+
borderLeftWidth: 2,
|
|
347
|
+
borderLeftStyle: "solid",
|
|
348
|
+
borderLeftColor: active ? "var(--accent)" : "transparent",
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
{selected ? <SelectionLight /> : null}
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
onClick={onToggle}
|
|
355
|
+
aria-expanded={open}
|
|
356
|
+
aria-label={`${open ? "Collapse" : "Expand"} ${label}`}
|
|
357
|
+
data-tree-toggle
|
|
358
|
+
className="flex shrink-0 items-center pr-1"
|
|
359
|
+
>
|
|
360
|
+
<Chevron open={open} />
|
|
361
|
+
</button>
|
|
362
|
+
<NavLink
|
|
363
|
+
to={rowTarget(to, selId)}
|
|
364
|
+
end
|
|
365
|
+
data-sel={selId}
|
|
366
|
+
data-nav-item
|
|
367
|
+
data-tree-row
|
|
368
|
+
data-tree-depth={depth}
|
|
369
|
+
data-tree-open={open}
|
|
370
|
+
onClick={onClick}
|
|
371
|
+
className={`flex min-w-0 items-center gap-1.5 py-[3px] pr-2 ${
|
|
372
|
+
end ? "shrink" : "flex-1"
|
|
373
|
+
}`}
|
|
374
|
+
>
|
|
375
|
+
{children}
|
|
376
|
+
{right && !end ? (
|
|
377
|
+
<span className="ml-auto shrink-0">{right}</span>
|
|
378
|
+
) : null}
|
|
379
|
+
</NavLink>
|
|
380
|
+
{end}
|
|
381
|
+
{right && end ? (
|
|
382
|
+
<span className="flex shrink-0 items-center pr-2">{right}</span>
|
|
383
|
+
) : null}
|
|
384
|
+
</div>
|
|
385
|
+
{under}
|
|
386
|
+
</>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A group header inside an aggregate - "value objects (3)". It names a kind
|
|
392
|
+
* rather than a thing, so it has no page and does nothing but open and close.
|
|
393
|
+
*/
|
|
394
|
+
function Group({
|
|
395
|
+
kind,
|
|
396
|
+
label,
|
|
397
|
+
count,
|
|
398
|
+
open,
|
|
399
|
+
onToggle,
|
|
400
|
+
depth,
|
|
401
|
+
children,
|
|
402
|
+
}: {
|
|
403
|
+
kind: LeafKind;
|
|
404
|
+
/** Overrides the kind's own plural — the stores group is "data", not "tables". */
|
|
405
|
+
label?: string;
|
|
406
|
+
count: number;
|
|
407
|
+
open: boolean;
|
|
408
|
+
onToggle: () => void;
|
|
409
|
+
depth: number;
|
|
410
|
+
children: React.ReactNode;
|
|
411
|
+
}) {
|
|
412
|
+
if (count === 0) return null;
|
|
413
|
+
return (
|
|
414
|
+
<>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={onToggle}
|
|
418
|
+
aria-expanded={open}
|
|
419
|
+
data-tree-row
|
|
420
|
+
data-tree-depth={depth}
|
|
421
|
+
data-tree-open={open}
|
|
422
|
+
style={{ paddingLeft: indent(depth) }}
|
|
423
|
+
className="tree-row group-row"
|
|
424
|
+
>
|
|
425
|
+
<Chevron open={open} />
|
|
426
|
+
<KindIcon kind={kind} />
|
|
427
|
+
<span>
|
|
428
|
+
{label ?? KIND_GROUP_LABEL[kind]} ({count})
|
|
429
|
+
</span>
|
|
430
|
+
</button>
|
|
431
|
+
<Unfold open={open}>{children}</Unfold>
|
|
432
|
+
</>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const KIND_GROUP_LABEL: Record<LeafKind, string> = {
|
|
437
|
+
vo: "value objects",
|
|
438
|
+
entity: "entities",
|
|
439
|
+
enum: "enums",
|
|
440
|
+
event: "events",
|
|
441
|
+
command: "commands",
|
|
442
|
+
query: "queries",
|
|
443
|
+
endpoint: "api",
|
|
444
|
+
table: "tables",
|
|
445
|
+
view: "views",
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Sections
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A top-level band of the tree. Its header folds it and always states its size:
|
|
454
|
+
* a folded section that also hid its count would be indistinguishable from an
|
|
455
|
+
* empty one, and the reader would have to open it to find out which.
|
|
456
|
+
*
|
|
457
|
+
* 12px above the header and nothing else between sections. The tree is one
|
|
458
|
+
* continuous list; boxing each band would make a reader count boxes.
|
|
459
|
+
*/
|
|
460
|
+
function Section({
|
|
461
|
+
title,
|
|
462
|
+
count,
|
|
463
|
+
open,
|
|
464
|
+
onToggle,
|
|
465
|
+
first = false,
|
|
466
|
+
children,
|
|
467
|
+
}: {
|
|
468
|
+
title: string;
|
|
469
|
+
count: number;
|
|
470
|
+
open: boolean;
|
|
471
|
+
onToggle: () => void;
|
|
472
|
+
/** The first section needs no air above it - the header is already there. */
|
|
473
|
+
first?: boolean;
|
|
474
|
+
children: React.ReactNode;
|
|
475
|
+
}) {
|
|
476
|
+
return (
|
|
477
|
+
<div className={first ? "" : "mt-3"}>
|
|
478
|
+
<button
|
|
479
|
+
type="button"
|
|
480
|
+
onClick={onToggle}
|
|
481
|
+
aria-expanded={open}
|
|
482
|
+
className="sb-section"
|
|
483
|
+
>
|
|
484
|
+
<Chevron open={open} />
|
|
485
|
+
{title}
|
|
486
|
+
<span className="sb-count">{count}</span>
|
|
487
|
+
</button>
|
|
488
|
+
<Unfold open={open}>{children}</Unfold>
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** A section of the tree with nothing under it, and why. */
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// The rows as the arrow keys see them. Every leaf, branch link and group
|
|
496
|
+
// header carries `data-tree-row`; a closed branch's children are not in the
|
|
497
|
+
// DOM at all, and a row mid-fold has no box yet, so what is left is exactly
|
|
498
|
+
// what is on screen.
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
function treeRowsOf(scroller: HTMLElement): HTMLElement[] {
|
|
502
|
+
return [...scroller.querySelectorAll<HTMLElement>("[data-tree-row]")].filter(
|
|
503
|
+
(el) => el.offsetParent !== null,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function describeRow(el: HTMLElement): TreeRow {
|
|
508
|
+
const open = el.dataset.treeOpen;
|
|
509
|
+
return {
|
|
510
|
+
depth: Number(el.dataset.treeDepth ?? 0),
|
|
511
|
+
open: open === "true" ? true : open === "false" ? false : null,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** The control that opens or closes a row: itself for a group, the chevron beside a branch. */
|
|
516
|
+
function toggleOf(row: HTMLElement): HTMLElement | null {
|
|
517
|
+
if (row.hasAttribute("aria-expanded")) return row;
|
|
518
|
+
return (
|
|
519
|
+
row.parentElement?.querySelector<HTMLElement>(":scope > [data-tree-toggle]") ??
|
|
520
|
+
null
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function TreeNote({ children }: { children: React.ReactNode }) {
|
|
525
|
+
return <div className="px-3 py-1 text-muted">{children}</div>;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Flows
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
const HEALTH_COLOR: Record<FlowHealth, string> = {
|
|
533
|
+
unresolved: "var(--status-unresolved)",
|
|
534
|
+
declared: "var(--fg-faint)",
|
|
535
|
+
verified: "var(--status-verified)",
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* One flow. Its name is what the row is for; the two marks at the end answer
|
|
540
|
+
* the two questions a reader has before opening it - how far does it travel,
|
|
541
|
+
* and can I believe it.
|
|
542
|
+
*/
|
|
543
|
+
function FlowRow({ entry }: { entry: FlowEntry }) {
|
|
544
|
+
const { flow, health, reach } = entry;
|
|
545
|
+
const { dots, more } = reachDots(reach);
|
|
546
|
+
const crosses =
|
|
547
|
+
reach.length === 0
|
|
548
|
+
? "stays inside its own context"
|
|
549
|
+
: `crosses into ${reach.join(", ")}`;
|
|
550
|
+
return (
|
|
551
|
+
<Leaf
|
|
552
|
+
to={paths.flow(flow.slug)}
|
|
553
|
+
depth={1}
|
|
554
|
+
title={`${flow.slug} — ${crosses}`}
|
|
555
|
+
>
|
|
556
|
+
<KindIcon kind="flow" />
|
|
557
|
+
<span className="truncate">{flow.name}</span>
|
|
558
|
+
<span className="ml-auto flex shrink-0 items-center gap-2 pl-2">
|
|
559
|
+
<span className="flex items-center gap-0.5" aria-hidden>
|
|
560
|
+
{dots.map((c) => (
|
|
561
|
+
<span key={c} className="sb-reach" style={ctxStyle(c)} />
|
|
562
|
+
))}
|
|
563
|
+
{more > 0 ? (
|
|
564
|
+
<span className="mono" style={{ color: "var(--fg-faint)" }}>
|
|
565
|
+
+{more}
|
|
566
|
+
</span>
|
|
567
|
+
) : null}
|
|
568
|
+
</span>
|
|
569
|
+
<span
|
|
570
|
+
className="sb-health"
|
|
571
|
+
title={FLOW_HEALTH_NOTE[health]}
|
|
572
|
+
style={{ background: HEALTH_COLOR[health] }}
|
|
573
|
+
/>
|
|
574
|
+
</span>
|
|
575
|
+
</Leaf>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The flows one context owns. Grouping them by owner rather than listing them
|
|
581
|
+
* flat is what makes the section answer "whose flows are these" - and the
|
|
582
|
+
* unowned group is a defect report: with a valid catalog it is never drawn.
|
|
583
|
+
*/
|
|
584
|
+
function FlowGroupNode({
|
|
585
|
+
group,
|
|
586
|
+
open,
|
|
587
|
+
onToggle,
|
|
588
|
+
}: {
|
|
589
|
+
group: FlowGroup;
|
|
590
|
+
open: boolean;
|
|
591
|
+
onToggle: () => void;
|
|
592
|
+
}) {
|
|
593
|
+
const { shown, hidden } = visibleEntries(group.entries);
|
|
594
|
+
const owner = group.owner;
|
|
595
|
+
const label = owner === null ? "unowned" : contextName(owner);
|
|
596
|
+
return (
|
|
597
|
+
<div>
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
onClick={onToggle}
|
|
601
|
+
aria-expanded={open}
|
|
602
|
+
style={{ paddingLeft: indent(0) }}
|
|
603
|
+
title={
|
|
604
|
+
owner === null
|
|
605
|
+
? "these flows name no owner — a valid catalog has none of these"
|
|
606
|
+
: `flows owned by ${owner}`
|
|
607
|
+
}
|
|
608
|
+
className="tree-row flex w-full items-center gap-1.5 py-[3px] pr-2 text-left t-micro transition-colors hover:bg-surface"
|
|
609
|
+
>
|
|
610
|
+
<Chevron open={open} />
|
|
611
|
+
{owner === null ? (
|
|
612
|
+
<TriangleAlert
|
|
613
|
+
size={13}
|
|
614
|
+
aria-hidden
|
|
615
|
+
className="block shrink-0 text-unresolved"
|
|
616
|
+
/>
|
|
617
|
+
) : (
|
|
618
|
+
<KindIcon kind="context" contextId={owner} />
|
|
619
|
+
)}
|
|
620
|
+
<span
|
|
621
|
+
className="truncate"
|
|
622
|
+
style={owner === null ? { color: "var(--status-unresolved)" } : {}}
|
|
623
|
+
>
|
|
624
|
+
{label}
|
|
625
|
+
</span>
|
|
626
|
+
<span className="sb-count">{group.entries.length}</span>
|
|
627
|
+
</button>
|
|
628
|
+
<Unfold open={open}>
|
|
629
|
+
<>
|
|
630
|
+
{shown.map((entry) => (
|
|
631
|
+
<FlowRow key={entry.flow.slug} entry={entry} />
|
|
632
|
+
))}
|
|
633
|
+
{hidden > 0 && owner !== null ? (
|
|
634
|
+
<NavLink
|
|
635
|
+
to={`${paths.flows()}?owner=${encodeURIComponent(owner)}`}
|
|
636
|
+
data-nav-item
|
|
637
|
+
style={{ paddingLeft: indent(1) }}
|
|
638
|
+
className="tree-row mono flex items-center py-[3px] pr-2 text-accent hover:bg-surface"
|
|
639
|
+
>
|
|
640
|
+
view all {group.entries.length} →
|
|
641
|
+
</NavLink>
|
|
642
|
+
) : null}
|
|
643
|
+
</>
|
|
644
|
+
</Unfold>
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Pins
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* The reader's own shortlist, above everything the catalog decided. It only
|
|
655
|
+
* exists while there is something on it: an empty band with a header would
|
|
656
|
+
* teach the feature by taking up room, which is the one thing the pane cannot
|
|
657
|
+
* spare.
|
|
658
|
+
*/
|
|
659
|
+
function PinnedSection({
|
|
660
|
+
open,
|
|
661
|
+
onToggle,
|
|
662
|
+
}: {
|
|
663
|
+
open: boolean;
|
|
664
|
+
onToggle: () => void;
|
|
665
|
+
}) {
|
|
666
|
+
const pins = usePinsStore((s) => s.pins);
|
|
667
|
+
const toggle = usePinsStore((s) => s.toggle);
|
|
668
|
+
const reorder = usePinsStore((s) => s.reorder);
|
|
669
|
+
// Which row is under the cursor is a ref, not state: a drop fires in the same
|
|
670
|
+
// gesture that started the drag, and a state write scheduled by `dragstart`
|
|
671
|
+
// is not guaranteed to have landed by the time `drop` reads it. The state
|
|
672
|
+
// beside it only dims the row, so it is allowed to arrive late.
|
|
673
|
+
const dragFrom = useRef<number | null>(null);
|
|
674
|
+
const [dragging, setDragging] = useState<number | null>(null);
|
|
675
|
+
|
|
676
|
+
// Resolved at render, so a pin taken against a build that no longer has the
|
|
677
|
+
// event simply stops being drawn rather than pointing into nothing.
|
|
678
|
+
const rows = useMemo(
|
|
679
|
+
() =>
|
|
680
|
+
pins
|
|
681
|
+
.map((pin, at) => ({ at, resolved: resolvePin(pin) }))
|
|
682
|
+
.filter(
|
|
683
|
+
(r): r is { at: number; resolved: NonNullable<typeof r.resolved> } =>
|
|
684
|
+
r.resolved !== null,
|
|
685
|
+
),
|
|
686
|
+
[pins],
|
|
687
|
+
);
|
|
688
|
+
if (rows.length === 0) return null;
|
|
689
|
+
|
|
690
|
+
return (
|
|
691
|
+
<Section
|
|
692
|
+
title="Pinned"
|
|
693
|
+
count={rows.length}
|
|
694
|
+
open={open}
|
|
695
|
+
onToggle={onToggle}
|
|
696
|
+
first
|
|
697
|
+
>
|
|
698
|
+
{rows.map(({ at, resolved }) => (
|
|
699
|
+
<div
|
|
700
|
+
key={`${resolved.pin.kind}:${resolved.pin.id}`}
|
|
701
|
+
draggable
|
|
702
|
+
onDragStart={(e) => {
|
|
703
|
+
dragFrom.current = at;
|
|
704
|
+
e.dataTransfer.effectAllowed = "move";
|
|
705
|
+
setDragging(at);
|
|
706
|
+
}}
|
|
707
|
+
onDragEnd={() => {
|
|
708
|
+
dragFrom.current = null;
|
|
709
|
+
setDragging(null);
|
|
710
|
+
}}
|
|
711
|
+
onDragOver={(e) => {
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
e.dataTransfer.dropEffect = "move";
|
|
714
|
+
}}
|
|
715
|
+
onDrop={(e) => {
|
|
716
|
+
e.preventDefault();
|
|
717
|
+
const from = dragFrom.current;
|
|
718
|
+
if (from !== null) reorder(from, at);
|
|
719
|
+
dragFrom.current = null;
|
|
720
|
+
setDragging(null);
|
|
721
|
+
}}
|
|
722
|
+
className={`group relative flex items-stretch ${
|
|
723
|
+
dragging === at ? "opacity-50" : ""
|
|
724
|
+
}`}
|
|
725
|
+
>
|
|
726
|
+
<NavLink
|
|
727
|
+
to={resolved.path}
|
|
728
|
+
end
|
|
729
|
+
draggable={false}
|
|
730
|
+
data-nav-item
|
|
731
|
+
title={resolved.title}
|
|
732
|
+
style={({ isActive }) => ({
|
|
733
|
+
paddingLeft: indent(0),
|
|
734
|
+
background: isActive ? "var(--surface-2)" : undefined,
|
|
735
|
+
borderLeftWidth: 2,
|
|
736
|
+
borderLeftStyle: "solid",
|
|
737
|
+
borderLeftColor: isActive ? "var(--accent)" : "transparent",
|
|
738
|
+
})}
|
|
739
|
+
className="tree-row flex min-w-0 flex-1 items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
|
|
740
|
+
>
|
|
741
|
+
<GripVertical
|
|
742
|
+
size={12}
|
|
743
|
+
aria-hidden
|
|
744
|
+
className="block shrink-0 opacity-0 group-hover:opacity-100"
|
|
745
|
+
style={{ color: "var(--fg-faint)" }}
|
|
746
|
+
/>
|
|
747
|
+
<KindIcon
|
|
748
|
+
kind={resolved.kind}
|
|
749
|
+
{...(resolved.contextId ? { contextId: resolved.contextId } : {})}
|
|
750
|
+
/>
|
|
751
|
+
<span className="truncate">{resolved.name}</span>
|
|
752
|
+
</NavLink>
|
|
753
|
+
<button
|
|
754
|
+
type="button"
|
|
755
|
+
onClick={() => toggle(resolved.pin)}
|
|
756
|
+
aria-label={`Unpin ${resolved.name}`}
|
|
757
|
+
title={`Unpin ${resolved.name}`}
|
|
758
|
+
className="flex shrink-0 items-center px-2 text-muted opacity-0 t-micro transition-opacity hover:text-ink group-hover:opacity-100 focus-visible:opacity-100"
|
|
759
|
+
>
|
|
760
|
+
<PinOff size={13} aria-hidden />
|
|
761
|
+
</button>
|
|
762
|
+
</div>
|
|
763
|
+
))}
|
|
764
|
+
</Section>
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
// The bottom group. A sibling of the scroller rather than a row inside it, so
|
|
770
|
+
// "pinned to the bottom" is a fact about the layout and not about how far the
|
|
771
|
+
// tree happens to have been scrolled.
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
function BottomGroup() {
|
|
775
|
+
// Both halves of the Problems page, because that is what the row opens. The
|
|
776
|
+
// badge is red while anything on it is an error and amber when only the
|
|
777
|
+
// schema disagrees with itself - a page of warnings is not a clean estate,
|
|
778
|
+
// and a green tick over it would be the one lie the row can tell.
|
|
779
|
+
const found = useMemo(
|
|
780
|
+
() => [
|
|
781
|
+
...problems(catalog),
|
|
782
|
+
...protoProblems(catalog, index),
|
|
783
|
+
...dataProblems(catalog, index),
|
|
784
|
+
],
|
|
785
|
+
[],
|
|
786
|
+
);
|
|
787
|
+
const errors = found.filter((p) => p.severity === "error").length;
|
|
788
|
+
const colour =
|
|
789
|
+
errors > 0 ? "var(--status-unresolved)" : "var(--status-declared)";
|
|
790
|
+
|
|
791
|
+
return (
|
|
792
|
+
<div className="shrink-0 border-t bg-canvas border-line py-0.5">
|
|
793
|
+
<NavLink
|
|
794
|
+
to={paths.problems()}
|
|
795
|
+
end
|
|
796
|
+
data-nav-item
|
|
797
|
+
aria-keyshortcuts="g p"
|
|
798
|
+
title={
|
|
799
|
+
found.length === 0
|
|
800
|
+
? "Problems — every edge in the catalog lands somewhere"
|
|
801
|
+
: `Problems — ${found.length} to look at`
|
|
802
|
+
}
|
|
803
|
+
style={({ isActive }) => ({
|
|
804
|
+
paddingLeft: indent(0),
|
|
805
|
+
background: isActive ? "var(--surface-2)" : undefined,
|
|
806
|
+
borderLeftWidth: 2,
|
|
807
|
+
borderLeftStyle: "solid",
|
|
808
|
+
borderLeftColor: isActive ? "var(--accent)" : "transparent",
|
|
809
|
+
})}
|
|
810
|
+
className="tree-row flex items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
|
|
811
|
+
>
|
|
812
|
+
<TriangleAlert
|
|
813
|
+
size={14}
|
|
814
|
+
aria-hidden
|
|
815
|
+
className="block shrink-0"
|
|
816
|
+
style={{ color: found.length === 0 ? "var(--fg-muted)" : colour }}
|
|
817
|
+
/>
|
|
818
|
+
<span className="truncate">Problems</span>
|
|
819
|
+
{found.length === 0 ? (
|
|
820
|
+
<Check
|
|
821
|
+
size={13}
|
|
822
|
+
aria-hidden
|
|
823
|
+
className="ml-auto block shrink-0 text-muted"
|
|
824
|
+
/>
|
|
825
|
+
) : (
|
|
826
|
+
<span
|
|
827
|
+
className="mono tnum ml-auto shrink-0"
|
|
828
|
+
style={{ color: colour }}
|
|
829
|
+
>
|
|
830
|
+
{found.length}
|
|
831
|
+
</span>
|
|
832
|
+
)}
|
|
833
|
+
</NavLink>
|
|
834
|
+
<NavLink
|
|
835
|
+
to={paths.settings()}
|
|
836
|
+
end
|
|
837
|
+
data-nav-item
|
|
838
|
+
title="Settings — projects, plugins and appearance"
|
|
839
|
+
style={({ isActive }) => ({
|
|
840
|
+
paddingLeft: indent(0),
|
|
841
|
+
background: isActive ? "var(--surface-2)" : undefined,
|
|
842
|
+
borderLeftWidth: 2,
|
|
843
|
+
borderLeftStyle: "solid",
|
|
844
|
+
borderLeftColor: isActive ? "var(--accent)" : "transparent",
|
|
845
|
+
})}
|
|
846
|
+
className="tree-row flex items-center gap-1.5 py-[3px] pr-2 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
|
|
847
|
+
>
|
|
848
|
+
<Settings2 size={14} aria-hidden className="block shrink-0" />
|
|
849
|
+
<span className="truncate">Settings</span>
|
|
850
|
+
</NavLink>
|
|
851
|
+
</div>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* What is left of the tree at 48px. Not a menu: every button here does the one
|
|
857
|
+
* thing the rail can honestly offer, which is to give the tree its width back
|
|
858
|
+
* and land the reader on the section they pointed at.
|
|
859
|
+
*/
|
|
860
|
+
function IconRail({ onExpand }: { onExpand: () => void }) {
|
|
861
|
+
const sections: { key: string; kind: Kind; label: string }[] = [
|
|
862
|
+
{ key: "flows", kind: "flow", label: "Flows" },
|
|
863
|
+
{ key: "domains", kind: "context", label: "Contexts" },
|
|
864
|
+
// Conditional for the same reason the band is: at 48px a button that opens
|
|
865
|
+
// an empty section is worse than no button.
|
|
866
|
+
...(allModules(catalog).length > 0
|
|
867
|
+
? [{ key: "registry", kind: "module" as Kind, label: "Registry" }]
|
|
868
|
+
: []),
|
|
869
|
+
{ key: "adrs", kind: "adr", label: "Decisions" },
|
|
870
|
+
];
|
|
871
|
+
return (
|
|
872
|
+
<nav
|
|
873
|
+
className="flex h-full flex-col items-center gap-1 border-r py-3 border-line bg-canvas"
|
|
874
|
+
aria-label="Catalog (collapsed)"
|
|
875
|
+
>
|
|
876
|
+
{/* The mark survives the collapse; the wordmark does not fit and is not
|
|
877
|
+
missed - at 48px the rose is the whole identity. */}
|
|
878
|
+
<CompassRose size={18} className="mb-1 text-ink" />
|
|
879
|
+
<button
|
|
880
|
+
type="button"
|
|
881
|
+
onClick={onExpand}
|
|
882
|
+
title="Expand the catalog"
|
|
883
|
+
aria-label="Expand the catalog"
|
|
884
|
+
aria-expanded={false}
|
|
885
|
+
className="flex size-8 items-center justify-center rounded-control text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
|
|
886
|
+
>
|
|
887
|
+
<PanelLeftOpen size={16} aria-hidden />
|
|
888
|
+
</button>
|
|
889
|
+
<span aria-hidden className="my-1 h-px w-6 bg-line" />
|
|
890
|
+
{sections.map(({ key, kind, label }) => (
|
|
891
|
+
<button
|
|
892
|
+
key={key}
|
|
893
|
+
type="button"
|
|
894
|
+
onClick={onExpand}
|
|
895
|
+
title={`${label} — expand the catalog`}
|
|
896
|
+
aria-label={`${label} — expand the catalog`}
|
|
897
|
+
className="flex size-8 items-center justify-center rounded-control t-micro transition-colors hover:bg-surface"
|
|
898
|
+
>
|
|
899
|
+
<KindIcon kind={kind} size={16} />
|
|
900
|
+
</button>
|
|
901
|
+
))}
|
|
902
|
+
<NavLink
|
|
903
|
+
to={paths.settings()}
|
|
904
|
+
title="Settings"
|
|
905
|
+
aria-label="Settings"
|
|
906
|
+
className={({ isActive }) =>
|
|
907
|
+
`mt-auto flex size-8 items-center justify-center rounded-control t-micro transition-colors hover:bg-surface ${
|
|
908
|
+
isActive ? "bg-surface text-accent" : "text-muted hover:text-ink"
|
|
909
|
+
}`
|
|
910
|
+
}
|
|
911
|
+
>
|
|
912
|
+
<Settings2 size={16} aria-hidden />
|
|
913
|
+
</NavLink>
|
|
914
|
+
</nav>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
919
|
+
|
|
920
|
+
export function Sidebar({
|
|
921
|
+
railed = false,
|
|
922
|
+
onExpand,
|
|
923
|
+
}: {
|
|
924
|
+
/** True once the shell panel has been collapsed to the 48px rail. */
|
|
925
|
+
railed?: boolean;
|
|
926
|
+
onExpand?: () => void;
|
|
927
|
+
}) {
|
|
928
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
929
|
+
|
|
930
|
+
// Which bands and which owner groups are folded. Both outlive the session:
|
|
931
|
+
// they are a reader's standing answer to "I do not work on that", unlike the
|
|
932
|
+
// branch state below, which follows whatever is selected.
|
|
933
|
+
const [sections, setSections] = useState<Flags>(() =>
|
|
934
|
+
readFlags(SECTIONS_KEY),
|
|
935
|
+
);
|
|
936
|
+
const [groups, setGroups] = useState<Flags>(() => readFlags(FLOW_GROUPS_KEY));
|
|
937
|
+
|
|
938
|
+
const sectionOpen = (key: string): boolean => sections[key] ?? true;
|
|
939
|
+
const toggleSection = (key: string) =>
|
|
940
|
+
setSections((prev) => {
|
|
941
|
+
const next = { ...prev, [key]: !(prev[key] ?? true) };
|
|
942
|
+
writeFlags(SECTIONS_KEY, next);
|
|
943
|
+
return next;
|
|
944
|
+
});
|
|
945
|
+
const groupOpen = (key: string): boolean => groups[key] ?? true;
|
|
946
|
+
const toggleGroup = (key: string) =>
|
|
947
|
+
setGroups((prev) => {
|
|
948
|
+
const next = { ...prev, [key]: !(prev[key] ?? true) };
|
|
949
|
+
writeFlags(FLOW_GROUPS_KEY, next);
|
|
950
|
+
return next;
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const isOpen = (key: string, def: boolean): boolean => collapsed[key] ?? def;
|
|
954
|
+
const toggle = (key: string, def: boolean) =>
|
|
955
|
+
setCollapsed((c) => ({ ...c, [key]: !(c[key] ?? def) }));
|
|
956
|
+
const flowGroups = useMemo(() => groupFlowsByOwner(catalog.flows), []);
|
|
957
|
+
const flowCount = flowGroups.reduce((n, g) => n + g.entries.length, 0);
|
|
958
|
+
|
|
959
|
+
// One row per vocabulary, not per word: thirty-eight terms in the tree would
|
|
960
|
+
// be a dictionary nobody scrolls past to reach the model. The row opens the
|
|
961
|
+
// page already filtered to that context.
|
|
962
|
+
const vocabs = useMemo(() => vocabularies(catalog), []);
|
|
963
|
+
const termCount = vocabs.reduce((n, v) => n + v.terms.length, 0);
|
|
964
|
+
|
|
965
|
+
// Core first, then supporting, then generic, then the ones nobody has rated;
|
|
966
|
+
// alphabetical inside each. This is the only ordering classification does.
|
|
967
|
+
const contexts = useMemo(
|
|
968
|
+
() =>
|
|
969
|
+
[...catalog.contexts]
|
|
970
|
+
.sort(
|
|
971
|
+
(a, b) =>
|
|
972
|
+
classificationRank(a.classification) -
|
|
973
|
+
classificationRank(b.classification) ||
|
|
974
|
+
a.name.localeCompare(b.name),
|
|
975
|
+
)
|
|
976
|
+
.map(contextRows),
|
|
977
|
+
[],
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
// The schema modules. Nothing at all when the estate has never published a
|
|
981
|
+
// proto, which is what keeps the band from appearing empty.
|
|
982
|
+
const modules = useMemo(() => allModules(catalog), []);
|
|
983
|
+
|
|
984
|
+
// A standing list of what currently holds; every decision is on its page.
|
|
985
|
+
const adrs = useMemo(() => newestAccepted(catalog, 5), []);
|
|
986
|
+
|
|
987
|
+
// --- following the selection --------------------------------------------
|
|
988
|
+
|
|
989
|
+
const selection = useSelectionStore((s) => s.selection);
|
|
990
|
+
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
|
991
|
+
|
|
992
|
+
// The arrow keys, on whatever row has focus. The rows describe themselves
|
|
993
|
+
// - depth, and open or closed for a branch - and `treeKey` says what the
|
|
994
|
+
// key does; this only reads the rows off the DOM and carries it out. A
|
|
995
|
+
// branch's toggle is the chevron beside its link; a group toggles itself.
|
|
996
|
+
const onTreeKey = useCallback((e: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
997
|
+
if (!TREE_KEYS.has(e.key)) return;
|
|
998
|
+
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
999
|
+
const scroller = scrollerRef.current;
|
|
1000
|
+
if (!scroller) return;
|
|
1001
|
+
const rows = treeRowsOf(scroller);
|
|
1002
|
+
const at = rows.indexOf(document.activeElement as HTMLElement);
|
|
1003
|
+
const move = treeKey(rows.map(describeRow), at, e.key);
|
|
1004
|
+
if (!move) return;
|
|
1005
|
+
e.preventDefault();
|
|
1006
|
+
const row = rows[move.index];
|
|
1007
|
+
if (!row) return;
|
|
1008
|
+
if (move.type === "focus") {
|
|
1009
|
+
row.focus();
|
|
1010
|
+
row.scrollIntoView({ block: "nearest" });
|
|
1011
|
+
} else {
|
|
1012
|
+
toggleOf(row)?.click();
|
|
1013
|
+
}
|
|
1014
|
+
}, []);
|
|
1015
|
+
|
|
1016
|
+
// Opening the ancestors is not optional. A selection made on a diagram or in
|
|
1017
|
+
// the palette has to be findable in the tree, and a tree row inside three
|
|
1018
|
+
// collapsed parents may as well not exist.
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
if (!selection) return;
|
|
1021
|
+
const resolved = resolveSelection(selection.id);
|
|
1022
|
+
if (!resolved) return;
|
|
1023
|
+
|
|
1024
|
+
const keys: string[] = [];
|
|
1025
|
+
if ("context" in resolved) keys.push(`c:${resolved.context.id}`);
|
|
1026
|
+
if ("service" in resolved) keys.push(`s:${resolved.service.id}`);
|
|
1027
|
+
if ("aggregate" in resolved) keys.push(`a:${resolved.aggregate.id}`);
|
|
1028
|
+
if (resolved.kind === "event") {
|
|
1029
|
+
keys.push(`a:${resolved.aggregate.id}:event`);
|
|
1030
|
+
}
|
|
1031
|
+
// A store sits behind two closed groups by default, and a table behind
|
|
1032
|
+
// three; a row nobody can see is a selection nobody can follow.
|
|
1033
|
+
if ("store" in resolved) {
|
|
1034
|
+
keys.push(`s:${resolved.service.id}:data`, `st:${resolved.store.id}`);
|
|
1035
|
+
}
|
|
1036
|
+
// A module has no ancestor to open - it sits at the top of its own band -
|
|
1037
|
+
// so only the band itself has to be unfolded for the row to be findable.
|
|
1038
|
+
const band = resolved.kind === "module" ? "registry" : "contexts";
|
|
1039
|
+
|
|
1040
|
+
if (keys.length === 0 && band === "contexts") return;
|
|
1041
|
+
|
|
1042
|
+
// The band itself can be folded, and folding it hides every ancestor at
|
|
1043
|
+
// once - so the section is opened first, then the branches inside it.
|
|
1044
|
+
setSections((prev) => {
|
|
1045
|
+
if (prev[band] !== false) return prev;
|
|
1046
|
+
const next = { ...prev, [band]: true };
|
|
1047
|
+
writeFlags(SECTIONS_KEY, next);
|
|
1048
|
+
return next;
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (keys.length === 0) return;
|
|
1052
|
+
|
|
1053
|
+
setCollapsed((c) => {
|
|
1054
|
+
// Only skip when every ancestor is *explicitly* open. An unset key means
|
|
1055
|
+
// "still on its default", which for an aggregate is closed.
|
|
1056
|
+
if (keys.every((k) => c[k] === true)) return c;
|
|
1057
|
+
const next = { ...c };
|
|
1058
|
+
for (const k of keys) next[k] = true;
|
|
1059
|
+
return next;
|
|
1060
|
+
});
|
|
1061
|
+
}, [selection]);
|
|
1062
|
+
|
|
1063
|
+
// Scroll only when the row is actually out of sight: yanking the tree under
|
|
1064
|
+
// a reader who can already see the row is worse than doing nothing.
|
|
1065
|
+
//
|
|
1066
|
+
// Measured one panel duration later, not now: "reveal" opens the row's
|
|
1067
|
+
// ancestors in the same update, and a branch takes that long to unfold to
|
|
1068
|
+
// its height. Measured at once, the row is inside a box still growing, and
|
|
1069
|
+
// where it is is not where it will be.
|
|
1070
|
+
useEffect(() => {
|
|
1071
|
+
const timer = setTimeout(() => {
|
|
1072
|
+
const scroller = scrollerRef.current;
|
|
1073
|
+
if (!selection || !scroller) return;
|
|
1074
|
+
const row = scroller.querySelector<HTMLElement>(
|
|
1075
|
+
`[data-sel="${CSS.escape(selection.id)}"]`,
|
|
1076
|
+
);
|
|
1077
|
+
if (!row) return;
|
|
1078
|
+
const a = row.getBoundingClientRect();
|
|
1079
|
+
const b = scroller.getBoundingClientRect();
|
|
1080
|
+
if (a.top >= b.top && a.bottom <= b.bottom) return;
|
|
1081
|
+
row.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
1082
|
+
}, DURATION.panel * 1000);
|
|
1083
|
+
return () => clearTimeout(timer);
|
|
1084
|
+
}, [selection, collapsed, sections, groups]);
|
|
1085
|
+
|
|
1086
|
+
if (railed) return <IconRail onExpand={() => onExpand?.()} />;
|
|
1087
|
+
|
|
1088
|
+
// Namespaces the selection light's layoutId to this tree.
|
|
1089
|
+
const treeId = useId();
|
|
1090
|
+
|
|
1091
|
+
return (
|
|
1092
|
+
<nav
|
|
1093
|
+
className="flex h-full flex-col border-r border-line bg-canvas"
|
|
1094
|
+
aria-label="Catalog"
|
|
1095
|
+
>
|
|
1096
|
+
<LayoutGroup id={treeId}>
|
|
1097
|
+
{/* One row: the mark and the name. Searching lives in ⌘K, so nothing
|
|
1098
|
+
else competes for the one pane that is always on screen. */}
|
|
1099
|
+
<div className="shrink-0 border-b px-3 pt-2.5 pb-2 border-line">
|
|
1100
|
+
<Wordmark />
|
|
1101
|
+
</div>
|
|
1102
|
+
|
|
1103
|
+
<div
|
|
1104
|
+
ref={scrollerRef}
|
|
1105
|
+
data-nav-list
|
|
1106
|
+
onKeyDown={onTreeKey}
|
|
1107
|
+
className="pane flex-1 overflow-y-auto pb-3"
|
|
1108
|
+
>
|
|
1109
|
+
<PinnedSection
|
|
1110
|
+
open={sectionOpen("pinned")}
|
|
1111
|
+
onToggle={() => toggleSection("pinned")}
|
|
1112
|
+
/>
|
|
1113
|
+
|
|
1114
|
+
<Section
|
|
1115
|
+
title="Flows"
|
|
1116
|
+
count={flowCount}
|
|
1117
|
+
open={sectionOpen("flows")}
|
|
1118
|
+
onToggle={() => toggleSection("flows")}
|
|
1119
|
+
>
|
|
1120
|
+
{flowGroups.length === 0 ? (
|
|
1121
|
+
<TreeNote>none charted yet</TreeNote>
|
|
1122
|
+
) : null}
|
|
1123
|
+
{flowGroups.map((group) => {
|
|
1124
|
+
const key = group.owner ?? "unowned";
|
|
1125
|
+
return (
|
|
1126
|
+
<FlowGroupNode
|
|
1127
|
+
key={key}
|
|
1128
|
+
group={group}
|
|
1129
|
+
open={groupOpen(key)}
|
|
1130
|
+
onToggle={() => toggleGroup(key)}
|
|
1131
|
+
/>
|
|
1132
|
+
);
|
|
1133
|
+
})}
|
|
1134
|
+
</Section>
|
|
1135
|
+
|
|
1136
|
+
<Section
|
|
1137
|
+
title="Contexts"
|
|
1138
|
+
count={contexts.length}
|
|
1139
|
+
open={sectionOpen("contexts")}
|
|
1140
|
+
onToggle={() => toggleSection("contexts")}
|
|
1141
|
+
>
|
|
1142
|
+
{contexts.length === 0 ? (
|
|
1143
|
+
<TreeNote>
|
|
1144
|
+
nothing extracted yet
|
|
1145
|
+
</TreeNote>
|
|
1146
|
+
) : null}
|
|
1147
|
+
{contexts.map(({ context, services }) => {
|
|
1148
|
+
const ckey = `c:${context.id}`;
|
|
1149
|
+
const copen = isOpen(ckey, true);
|
|
1150
|
+
const unresolved = contextStats(context).unresolved;
|
|
1151
|
+
return (
|
|
1152
|
+
<div key={context.id}>
|
|
1153
|
+
<Branch
|
|
1154
|
+
to={paths.context(context.id)}
|
|
1155
|
+
depth={0}
|
|
1156
|
+
open={copen}
|
|
1157
|
+
onToggle={() => toggle(ckey, true)}
|
|
1158
|
+
label={`context ${context.id}`}
|
|
1159
|
+
selId={context.id}
|
|
1160
|
+
end={
|
|
1161
|
+
<>
|
|
1162
|
+
{/* Its own target, because it goes somewhere else: the
|
|
1163
|
+
reader clicked the count, and the count is a list. */}
|
|
1164
|
+
{unresolved > 0 ? (
|
|
1165
|
+
<Link
|
|
1166
|
+
to={`${paths.problems()}?context=${encodeURIComponent(context.id)}`}
|
|
1167
|
+
title={`${unresolved} unresolved ${plural(unresolved, "edge")} — open Problems`}
|
|
1168
|
+
className="mono tnum flex shrink-0 items-center px-1.5 text-unresolved hover:underline"
|
|
1169
|
+
>
|
|
1170
|
+
{unresolved}
|
|
1171
|
+
</Link>
|
|
1172
|
+
) : null}
|
|
1173
|
+
<span className="ml-auto" />
|
|
1174
|
+
{/* Only worth saying while the services are not on
|
|
1175
|
+
screen; once the branch is open the reader can count
|
|
1176
|
+
them. */}
|
|
1177
|
+
{!copen ? (
|
|
1178
|
+
<span className="mono flex shrink-0 items-center pr-1.5 text-muted opacity-0 t-micro transition-opacity group-hover:opacity-100">
|
|
1179
|
+
{context.services.length}{" "}
|
|
1180
|
+
{plural(context.services.length, "service")}
|
|
1181
|
+
</span>
|
|
1182
|
+
) : null}
|
|
1183
|
+
</>
|
|
1184
|
+
}
|
|
1185
|
+
right={
|
|
1186
|
+
context.classification ? (
|
|
1187
|
+
<span
|
|
1188
|
+
className="sb-class"
|
|
1189
|
+
title={CLASSIFICATION_NOTE[context.classification]}
|
|
1190
|
+
>
|
|
1191
|
+
{context.classification}
|
|
1192
|
+
</span>
|
|
1193
|
+
) : null
|
|
1194
|
+
}
|
|
1195
|
+
>
|
|
1196
|
+
<KindIcon kind="context" contextId={context.id} />
|
|
1197
|
+
<span className="truncate" title={context.id}>
|
|
1198
|
+
{context.name}
|
|
1199
|
+
</span>
|
|
1200
|
+
</Branch>
|
|
1201
|
+
<Unfold open={copen}>
|
|
1202
|
+
{services.map(
|
|
1203
|
+
({ service, aggregates, endpoints, stores }) => {
|
|
1204
|
+
const skey = `s:${service.id}`;
|
|
1205
|
+
const sopen = isOpen(skey, true);
|
|
1206
|
+
return (
|
|
1207
|
+
<div key={service.id}>
|
|
1208
|
+
<Branch
|
|
1209
|
+
to={paths.service(context.id, service.slug)}
|
|
1210
|
+
depth={1}
|
|
1211
|
+
open={sopen}
|
|
1212
|
+
onToggle={() => toggle(skey, true)}
|
|
1213
|
+
label={`service ${service.id}`}
|
|
1214
|
+
selId={service.id}
|
|
1215
|
+
>
|
|
1216
|
+
<KindIcon kind="service" />
|
|
1217
|
+
<span className="truncate" title={service.id}>
|
|
1218
|
+
{service.name}
|
|
1219
|
+
</span>
|
|
1220
|
+
</Branch>
|
|
1221
|
+
<Unfold open={sopen}>
|
|
1222
|
+
{aggregates.map((rows) => (
|
|
1223
|
+
<AggregateNode
|
|
1224
|
+
key={rows.aggregate.id}
|
|
1225
|
+
rows={rows}
|
|
1226
|
+
contextId={context.id}
|
|
1227
|
+
serviceSlug={service.slug}
|
|
1228
|
+
isOpen={isOpen}
|
|
1229
|
+
toggle={toggle}
|
|
1230
|
+
/>
|
|
1231
|
+
))}
|
|
1232
|
+
{/* Between the model and where it is kept: an
|
|
1233
|
+
endpoint is how the outside reaches the first and
|
|
1234
|
+
eventually moves the second. Methods are listed
|
|
1235
|
+
flat rather than nested under their interface -
|
|
1236
|
+
an operationId is unique across a document, so
|
|
1237
|
+
the extra level would carry no information and
|
|
1238
|
+
cost a line of indent in a narrow tree. */}
|
|
1239
|
+
{endpoints.length > 0 ? (
|
|
1240
|
+
<Group
|
|
1241
|
+
kind="endpoint"
|
|
1242
|
+
label="api"
|
|
1243
|
+
count={endpoints.reduce(
|
|
1244
|
+
(n, e) => n + e.methods.length,
|
|
1245
|
+
0,
|
|
1246
|
+
)}
|
|
1247
|
+
depth={2}
|
|
1248
|
+
open={isOpen(`${skey}:api`, false)}
|
|
1249
|
+
onToggle={() => toggle(`${skey}:api`, false)}
|
|
1250
|
+
>
|
|
1251
|
+
{endpoints.flatMap(({ provided, methods }) =>
|
|
1252
|
+
methods.map((method) => (
|
|
1253
|
+
<Leaf
|
|
1254
|
+
key={`${provided.id}/${method.name}`}
|
|
1255
|
+
to={`${paths.service(context.id, service.slug)}?tab=provides`}
|
|
1256
|
+
depth={3}
|
|
1257
|
+
title={`${provided.id}/${method.name} — ${provided.source}`}
|
|
1258
|
+
>
|
|
1259
|
+
<KindIcon kind="endpoint" />
|
|
1260
|
+
<span className="mono truncate">
|
|
1261
|
+
{method.name}
|
|
1262
|
+
</span>
|
|
1263
|
+
</Leaf>
|
|
1264
|
+
)),
|
|
1265
|
+
)}
|
|
1266
|
+
</Group>
|
|
1267
|
+
) : null}
|
|
1268
|
+
|
|
1269
|
+
{/* After the aggregates, because the model comes
|
|
1270
|
+
before where it is kept. Closed by default: this
|
|
1271
|
+
is the answer to a question about deployment, not
|
|
1272
|
+
the one the tree is usually open for. */}
|
|
1273
|
+
<Group
|
|
1274
|
+
kind="table"
|
|
1275
|
+
label="data"
|
|
1276
|
+
count={stores.length}
|
|
1277
|
+
depth={2}
|
|
1278
|
+
open={isOpen(`${skey}:data`, false)}
|
|
1279
|
+
onToggle={() => toggle(`${skey}:data`, false)}
|
|
1280
|
+
>
|
|
1281
|
+
{stores.map(({ store, tables, views }) => (
|
|
1282
|
+
<StoreNode
|
|
1283
|
+
key={store.id}
|
|
1284
|
+
store={store}
|
|
1285
|
+
tables={tables}
|
|
1286
|
+
views={views}
|
|
1287
|
+
contextId={context.id}
|
|
1288
|
+
serviceSlug={service.slug}
|
|
1289
|
+
isOpen={isOpen}
|
|
1290
|
+
toggle={toggle}
|
|
1291
|
+
/>
|
|
1292
|
+
))}
|
|
1293
|
+
</Group>
|
|
1294
|
+
</Unfold>
|
|
1295
|
+
</div>
|
|
1296
|
+
);
|
|
1297
|
+
},
|
|
1298
|
+
)}
|
|
1299
|
+
</Unfold>
|
|
1300
|
+
</div>
|
|
1301
|
+
);
|
|
1302
|
+
})}
|
|
1303
|
+
</Section>
|
|
1304
|
+
|
|
1305
|
+
{/* Guarded on the count rather than drawn empty: `Section` renders its
|
|
1306
|
+
header and a TreeNote at zero, so an estate that has never published
|
|
1307
|
+
a proto would grow a permanent dead row. */}
|
|
1308
|
+
{modules.length > 0 ? (
|
|
1309
|
+
<Section
|
|
1310
|
+
title="Registry"
|
|
1311
|
+
count={modules.length}
|
|
1312
|
+
open={sectionOpen("registry")}
|
|
1313
|
+
onToggle={() => toggleSection("registry")}
|
|
1314
|
+
>
|
|
1315
|
+
{modules.map((module) => {
|
|
1316
|
+
const open = isOpen(`mod:${module.id}`, false);
|
|
1317
|
+
|
|
1318
|
+
return (
|
|
1319
|
+
<div key={module.id}>
|
|
1320
|
+
<Branch
|
|
1321
|
+
to={paths.module(module.slug)}
|
|
1322
|
+
depth={0}
|
|
1323
|
+
selId={module.id}
|
|
1324
|
+
label={`module ${module.name}`}
|
|
1325
|
+
open={open}
|
|
1326
|
+
onToggle={() => toggle(`mod:${module.id}`, false)}
|
|
1327
|
+
right={
|
|
1328
|
+
module.commit ? null : (
|
|
1329
|
+
<span
|
|
1330
|
+
className="mono text-muted"
|
|
1331
|
+
title="tracked by label rather than pinned to a commit"
|
|
1332
|
+
>
|
|
1333
|
+
~
|
|
1334
|
+
</span>
|
|
1335
|
+
)
|
|
1336
|
+
}
|
|
1337
|
+
>
|
|
1338
|
+
<KindIcon kind="module" />
|
|
1339
|
+
<span className="mono truncate" title={module.id}>
|
|
1340
|
+
{module.name}
|
|
1341
|
+
</span>
|
|
1342
|
+
</Branch>
|
|
1343
|
+
|
|
1344
|
+
{/* Two levels only. The interfaces inside a module are
|
|
1345
|
+
already in the tree under the service that answers on
|
|
1346
|
+
them, and listing them again would say the estate has
|
|
1347
|
+
twice as many. */}
|
|
1348
|
+
{open
|
|
1349
|
+
? module.packages.map((name) => (
|
|
1350
|
+
<Leaf
|
|
1351
|
+
key={name}
|
|
1352
|
+
to={`${paths.module(module.slug)}?tab=interfaces#${packageAnchor(name)}`}
|
|
1353
|
+
depth={1}
|
|
1354
|
+
title={`${name} — ${module.id}`}
|
|
1355
|
+
>
|
|
1356
|
+
<KindIcon kind="endpoint" />
|
|
1357
|
+
<span className="mono truncate">{name}</span>
|
|
1358
|
+
</Leaf>
|
|
1359
|
+
))
|
|
1360
|
+
: null}
|
|
1361
|
+
</div>
|
|
1362
|
+
);
|
|
1363
|
+
})}
|
|
1364
|
+
<NavLink
|
|
1365
|
+
to={paths.registry()}
|
|
1366
|
+
data-nav-item
|
|
1367
|
+
className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
|
|
1368
|
+
>
|
|
1369
|
+
view all {allModules(catalog).length} →
|
|
1370
|
+
</NavLink>
|
|
1371
|
+
</Section>
|
|
1372
|
+
) : null}
|
|
1373
|
+
|
|
1374
|
+
<Section
|
|
1375
|
+
title="Decisions"
|
|
1376
|
+
count={adrs.length}
|
|
1377
|
+
open={sectionOpen("decisions")}
|
|
1378
|
+
onToggle={() => toggleSection("decisions")}
|
|
1379
|
+
>
|
|
1380
|
+
{adrs.length === 0 ? (
|
|
1381
|
+
<TreeNote>
|
|
1382
|
+
nothing on the record yet
|
|
1383
|
+
</TreeNote>
|
|
1384
|
+
) : null}
|
|
1385
|
+
{adrs.map((adr) => (
|
|
1386
|
+
<Leaf
|
|
1387
|
+
key={adr.id}
|
|
1388
|
+
to={paths.adr(adr.slug)}
|
|
1389
|
+
depth={0}
|
|
1390
|
+
title={`${adr.id} — ${adr.title}`}
|
|
1391
|
+
>
|
|
1392
|
+
<KindIcon kind="adr" />
|
|
1393
|
+
{/* The number without its prefix: the header already said
|
|
1394
|
+
DECISIONS, and "ADR-" repeated down a column is a word the
|
|
1395
|
+
reader reads five times to get to the digits. */}
|
|
1396
|
+
<span
|
|
1397
|
+
className={`mono tnum shrink-0 ${isStruck(adr.status) ? "line-through text-muted" : ""}`}
|
|
1398
|
+
>
|
|
1399
|
+
{adrNumber(adr).replace(/^ADR-/, "")}
|
|
1400
|
+
</span>
|
|
1401
|
+
<span className="truncate">{adr.title}</span>
|
|
1402
|
+
</Leaf>
|
|
1403
|
+
))}
|
|
1404
|
+
{catalog.adrs.length > 0 ? (
|
|
1405
|
+
<NavLink
|
|
1406
|
+
to={paths.adrs()}
|
|
1407
|
+
data-nav-item
|
|
1408
|
+
className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
|
|
1409
|
+
>
|
|
1410
|
+
view all {catalog.adrs.length} →
|
|
1411
|
+
</NavLink>
|
|
1412
|
+
) : null}
|
|
1413
|
+
</Section>
|
|
1414
|
+
|
|
1415
|
+
{/* Guarded on the count, like Registry: an estate that has written no
|
|
1416
|
+
glossary would otherwise grow a permanent empty band teaching a
|
|
1417
|
+
feature by taking up room. */}
|
|
1418
|
+
{allTerms(catalog).length > 0 ? (
|
|
1419
|
+
<Section
|
|
1420
|
+
title="Language"
|
|
1421
|
+
count={termCount}
|
|
1422
|
+
open={sectionOpen("language")}
|
|
1423
|
+
onToggle={() => toggleSection("language")}
|
|
1424
|
+
>
|
|
1425
|
+
{vocabs.map((vocabulary) => (
|
|
1426
|
+
<Leaf
|
|
1427
|
+
key={vocabulary.contextId}
|
|
1428
|
+
to={paths.contextLanguage(vocabulary.contextId)}
|
|
1429
|
+
depth={0}
|
|
1430
|
+
title={`${vocabulary.context?.name ?? vocabulary.contextId} — one meaning per word inside this context`}
|
|
1431
|
+
>
|
|
1432
|
+
<KindIcon kind="context" contextId={vocabulary.contextId} />
|
|
1433
|
+
<span className="truncate">{vocabulary.contextId}</span>
|
|
1434
|
+
<span className="sb-count">{vocabulary.terms.length}</span>
|
|
1435
|
+
</Leaf>
|
|
1436
|
+
))}
|
|
1437
|
+
<NavLink
|
|
1438
|
+
to={paths.language()}
|
|
1439
|
+
data-nav-item
|
|
1440
|
+
className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
|
|
1441
|
+
>
|
|
1442
|
+
view all {allTerms(catalog).length} →
|
|
1443
|
+
</NavLink>
|
|
1444
|
+
</Section>
|
|
1445
|
+
) : null}
|
|
1446
|
+
</div>
|
|
1447
|
+
|
|
1448
|
+
<BottomGroup />
|
|
1449
|
+
</LayoutGroup>
|
|
1450
|
+
</nav>
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* A store and the tables in it. The store row navigates to its own ER page; the
|
|
1456
|
+
* table rows land on that page with the table already selected, because a table
|
|
1457
|
+
* read outside the picture of what points at it is a list of column names.
|
|
1458
|
+
*/
|
|
1459
|
+
function StoreNode({
|
|
1460
|
+
store,
|
|
1461
|
+
tables,
|
|
1462
|
+
views,
|
|
1463
|
+
contextId,
|
|
1464
|
+
serviceSlug,
|
|
1465
|
+
isOpen,
|
|
1466
|
+
toggle,
|
|
1467
|
+
}: {
|
|
1468
|
+
store: Store;
|
|
1469
|
+
tables: Table[];
|
|
1470
|
+
views: View[];
|
|
1471
|
+
contextId: string;
|
|
1472
|
+
serviceSlug: string;
|
|
1473
|
+
isOpen: (key: string, def: boolean) => boolean;
|
|
1474
|
+
toggle: (key: string, def: boolean) => void;
|
|
1475
|
+
}) {
|
|
1476
|
+
const key = `st:${store.id}`;
|
|
1477
|
+
const open = isOpen(key, false);
|
|
1478
|
+
const to = paths.store(contextId, serviceSlug, store.slug);
|
|
1479
|
+
|
|
1480
|
+
return (
|
|
1481
|
+
<div>
|
|
1482
|
+
<Branch
|
|
1483
|
+
to={to}
|
|
1484
|
+
depth={3}
|
|
1485
|
+
open={open}
|
|
1486
|
+
onToggle={() => toggle(key, false)}
|
|
1487
|
+
label={`store ${store.id}`}
|
|
1488
|
+
selId={store.id}
|
|
1489
|
+
right={
|
|
1490
|
+
<span
|
|
1491
|
+
className="mono flex items-center gap-1 text-muted"
|
|
1492
|
+
title={STORE_KIND_LABEL[store.kind]}
|
|
1493
|
+
>
|
|
1494
|
+
<StoreKindMark kind={store.kind} size={12} />
|
|
1495
|
+
{store.kind}
|
|
1496
|
+
</span>
|
|
1497
|
+
}
|
|
1498
|
+
>
|
|
1499
|
+
<KindIcon kind="store" />
|
|
1500
|
+
<span className="truncate" title={store.id}>
|
|
1501
|
+
{store.name}
|
|
1502
|
+
</span>
|
|
1503
|
+
</Branch>
|
|
1504
|
+
{/* Tables and views keep the monospace: unlike everything above them
|
|
1505
|
+
they are not human names but the identifiers a migration writes and a
|
|
1506
|
+
query names, and reading one in prose type invites a typo. */}
|
|
1507
|
+
{open
|
|
1508
|
+
? tables.map((table) => (
|
|
1509
|
+
<Leaf
|
|
1510
|
+
key={table.id}
|
|
1511
|
+
to={to}
|
|
1512
|
+
depth={4}
|
|
1513
|
+
title={table.doc ?? table.id}
|
|
1514
|
+
selId={table.id}
|
|
1515
|
+
>
|
|
1516
|
+
<KindIcon kind="table" />
|
|
1517
|
+
<span className="mono truncate">{table.name}</span>
|
|
1518
|
+
{table.role && table.role !== "other" ? (
|
|
1519
|
+
<span className="mono ml-auto shrink-0 text-muted">
|
|
1520
|
+
{table.role === "aggregate-root" ? "root" : table.role}
|
|
1521
|
+
</span>
|
|
1522
|
+
) : null}
|
|
1523
|
+
</Leaf>
|
|
1524
|
+
))
|
|
1525
|
+
: null}
|
|
1526
|
+
{/* Views after the tables, always: they are computed from them, and a
|
|
1527
|
+
tree that interleaved the two would put a reading of the rows above
|
|
1528
|
+
the place the rows live. */}
|
|
1529
|
+
{open
|
|
1530
|
+
? views.map((view) => (
|
|
1531
|
+
<Leaf
|
|
1532
|
+
key={view.id}
|
|
1533
|
+
to={to}
|
|
1534
|
+
depth={4}
|
|
1535
|
+
title={view.doc ?? view.id}
|
|
1536
|
+
selId={view.id}
|
|
1537
|
+
>
|
|
1538
|
+
<KindIcon kind="view" />
|
|
1539
|
+
<span className="mono truncate">{view.name}</span>
|
|
1540
|
+
<span className="mono ml-auto shrink-0 text-muted">
|
|
1541
|
+
{view.materialized ? "matview" : "view"}
|
|
1542
|
+
</span>
|
|
1543
|
+
</Leaf>
|
|
1544
|
+
))
|
|
1545
|
+
: null}
|
|
1546
|
+
</div>
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function AggregateNode({
|
|
1551
|
+
rows,
|
|
1552
|
+
contextId,
|
|
1553
|
+
serviceSlug,
|
|
1554
|
+
isOpen,
|
|
1555
|
+
toggle,
|
|
1556
|
+
}: {
|
|
1557
|
+
rows: AggregateRows;
|
|
1558
|
+
contextId: string;
|
|
1559
|
+
serviceSlug: string;
|
|
1560
|
+
isOpen: (key: string, def: boolean) => boolean;
|
|
1561
|
+
toggle: (key: string, def: boolean) => void;
|
|
1562
|
+
}) {
|
|
1563
|
+
const { aggregate } = rows;
|
|
1564
|
+
const akey = `a:${aggregate.id}`;
|
|
1565
|
+
const aopen = isOpen(akey, false);
|
|
1566
|
+
const to = paths.aggregate(contextId, serviceSlug, aggregate.slug);
|
|
1567
|
+
|
|
1568
|
+
// Events are the group that opens by itself: they are what other contexts
|
|
1569
|
+
// actually depend on. The structural groups stay shut until asked for.
|
|
1570
|
+
const group = (kind: LeafKind, def: boolean) => ({
|
|
1571
|
+
kind,
|
|
1572
|
+
open: isOpen(`${akey}:${kind}`, def),
|
|
1573
|
+
onToggle: () => toggle(`${akey}:${kind}`, def),
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
const blockLeaf = (kind: "vo" | "entity", block: Block) => {
|
|
1577
|
+
const path =
|
|
1578
|
+
kind === "vo"
|
|
1579
|
+
? paths.valueObject(contextId, serviceSlug, aggregate.slug, block.slug)
|
|
1580
|
+
: paths.entity(contextId, serviceSlug, aggregate.slug, block.slug);
|
|
1581
|
+
return (
|
|
1582
|
+
<Leaf key={block.id} to={path} depth={4} title={block.doc ?? block.id}>
|
|
1583
|
+
<KindIcon kind={kind} />
|
|
1584
|
+
<span className="mono truncate">{block.name}</span>
|
|
1585
|
+
{kind === "entity" && block.name === aggregate.root ? (
|
|
1586
|
+
<span className="mono ml-auto shrink-0 text-muted">root</span>
|
|
1587
|
+
) : null}
|
|
1588
|
+
</Leaf>
|
|
1589
|
+
);
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
return (
|
|
1593
|
+
<div>
|
|
1594
|
+
<Branch
|
|
1595
|
+
to={to}
|
|
1596
|
+
depth={2}
|
|
1597
|
+
open={aopen}
|
|
1598
|
+
onToggle={() => toggle(akey, false)}
|
|
1599
|
+
label={`aggregate ${aggregate.name}`}
|
|
1600
|
+
selId={aggregate.id}
|
|
1601
|
+
under={
|
|
1602
|
+
<div
|
|
1603
|
+
className="mono truncate pr-2 text-muted"
|
|
1604
|
+
style={{ paddingLeft: indent(3), fontSize: 11 }}
|
|
1605
|
+
title={`aggregate root: ${aggregate.root}`}
|
|
1606
|
+
>
|
|
1607
|
+
root: {aggregate.root}
|
|
1608
|
+
</div>
|
|
1609
|
+
}
|
|
1610
|
+
right={
|
|
1611
|
+
aggregate.events.length === 0 ? (
|
|
1612
|
+
<span
|
|
1613
|
+
className="mono text-muted"
|
|
1614
|
+
title="this aggregate publishes no events"
|
|
1615
|
+
>
|
|
1616
|
+
0
|
|
1617
|
+
</span>
|
|
1618
|
+
) : null
|
|
1619
|
+
}
|
|
1620
|
+
>
|
|
1621
|
+
<KindIcon kind="aggregate" />
|
|
1622
|
+
<span className="truncate font-medium" title={aggregate.id}>
|
|
1623
|
+
{aggregate.name}
|
|
1624
|
+
</span>
|
|
1625
|
+
</Branch>
|
|
1626
|
+
|
|
1627
|
+
<Unfold open={aopen}>
|
|
1628
|
+
<>
|
|
1629
|
+
<Group
|
|
1630
|
+
{...group("vo", false)}
|
|
1631
|
+
count={rows.valueObjects.length}
|
|
1632
|
+
depth={3}
|
|
1633
|
+
>
|
|
1634
|
+
{rows.valueObjects.map((b) => blockLeaf("vo", b))}
|
|
1635
|
+
</Group>
|
|
1636
|
+
|
|
1637
|
+
<Group
|
|
1638
|
+
{...group("entity", false)}
|
|
1639
|
+
count={rows.entities.length}
|
|
1640
|
+
depth={3}
|
|
1641
|
+
>
|
|
1642
|
+
{rows.entities.map((b) => blockLeaf("entity", b))}
|
|
1643
|
+
</Group>
|
|
1644
|
+
|
|
1645
|
+
<Group
|
|
1646
|
+
{...group("enum", false)}
|
|
1647
|
+
count={rows.enums.length}
|
|
1648
|
+
depth={3}
|
|
1649
|
+
>
|
|
1650
|
+
{rows.enums.map((item) => (
|
|
1651
|
+
<Leaf
|
|
1652
|
+
key={item.id}
|
|
1653
|
+
to={paths.enum(contextId, serviceSlug, aggregate.slug, item.slug)}
|
|
1654
|
+
depth={4}
|
|
1655
|
+
title={item.doc || item.id}
|
|
1656
|
+
>
|
|
1657
|
+
<KindIcon kind="enum" />
|
|
1658
|
+
<span className="mono truncate">{item.name}</span>
|
|
1659
|
+
<span className="mono ml-auto shrink-0 text-muted">
|
|
1660
|
+
{item.values.length}
|
|
1661
|
+
</span>
|
|
1662
|
+
</Leaf>
|
|
1663
|
+
))}
|
|
1664
|
+
</Group>
|
|
1665
|
+
|
|
1666
|
+
<Group
|
|
1667
|
+
{...group("event", true)}
|
|
1668
|
+
count={rows.events.length}
|
|
1669
|
+
depth={3}
|
|
1670
|
+
>
|
|
1671
|
+
{rows.events.map((event) => {
|
|
1672
|
+
const latest = event.versions[event.versions.length - 1];
|
|
1673
|
+
return (
|
|
1674
|
+
<Leaf
|
|
1675
|
+
key={event.id}
|
|
1676
|
+
to={paths.event(
|
|
1677
|
+
contextId,
|
|
1678
|
+
serviceSlug,
|
|
1679
|
+
aggregate.slug,
|
|
1680
|
+
event.slug,
|
|
1681
|
+
)}
|
|
1682
|
+
depth={4}
|
|
1683
|
+
title={event.id}
|
|
1684
|
+
selId={event.id}
|
|
1685
|
+
>
|
|
1686
|
+
<KindIcon kind="event" />
|
|
1687
|
+
<span
|
|
1688
|
+
className="mono truncate"
|
|
1689
|
+
style={{ color: "var(--kind-event)" }}
|
|
1690
|
+
>
|
|
1691
|
+
{event.name}
|
|
1692
|
+
</span>
|
|
1693
|
+
{latest ? (
|
|
1694
|
+
<span className="mono ml-auto shrink-0 border px-1 border-line text-muted">
|
|
1695
|
+
{latest.version}
|
|
1696
|
+
</span>
|
|
1697
|
+
) : null}
|
|
1698
|
+
</Leaf>
|
|
1699
|
+
);
|
|
1700
|
+
})}
|
|
1701
|
+
</Group>
|
|
1702
|
+
|
|
1703
|
+
<Group
|
|
1704
|
+
{...group("command", false)}
|
|
1705
|
+
count={rows.commands.length}
|
|
1706
|
+
depth={3}
|
|
1707
|
+
>
|
|
1708
|
+
{rows.commands.map((op) => (
|
|
1709
|
+
<Leaf
|
|
1710
|
+
key={op.id}
|
|
1711
|
+
to={`${to}#bb-commands`}
|
|
1712
|
+
depth={4}
|
|
1713
|
+
title={op.id}
|
|
1714
|
+
>
|
|
1715
|
+
<KindIcon kind="command" />
|
|
1716
|
+
<span className="mono truncate text-muted">{op.id}</span>
|
|
1717
|
+
</Leaf>
|
|
1718
|
+
))}
|
|
1719
|
+
</Group>
|
|
1720
|
+
|
|
1721
|
+
<Group
|
|
1722
|
+
{...group("query", false)}
|
|
1723
|
+
count={rows.queries.length}
|
|
1724
|
+
depth={3}
|
|
1725
|
+
>
|
|
1726
|
+
{rows.queries.map((op) => (
|
|
1727
|
+
<Leaf
|
|
1728
|
+
key={op.id}
|
|
1729
|
+
to={`${to}#bb-queries`}
|
|
1730
|
+
depth={4}
|
|
1731
|
+
title={op.id}
|
|
1732
|
+
>
|
|
1733
|
+
<KindIcon kind="query" />
|
|
1734
|
+
<span className="mono truncate text-muted">{op.id}</span>
|
|
1735
|
+
</Leaf>
|
|
1736
|
+
))}
|
|
1737
|
+
</Group>
|
|
1738
|
+
</>
|
|
1739
|
+
</Unfold>
|
|
1740
|
+
</div>
|
|
1741
|
+
);
|
|
1742
|
+
}
|