@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,780 @@
|
|
|
1
|
+
// One table, everywhere.
|
|
2
|
+
//
|
|
3
|
+
// Index pages used to hand-write a <table> each, which meant each one sorted
|
|
4
|
+
// (or did not) in its own way, and a table in a README was a third thing
|
|
5
|
+
// again. This is the one primitive underneath all of them: the page declares
|
|
6
|
+
// what its columns HOLD, and this decides what that means - how they sort,
|
|
7
|
+
// which way they line up, what a cell looks like, what the toolbar offers.
|
|
8
|
+
//
|
|
9
|
+
// It owns the parts a page should not have to re-litigate: the sticky header,
|
|
10
|
+
// the density, the j/k list, the selected row, the widths the reader dragged.
|
|
11
|
+
// TanStack owns the models. Neither owns the markup, which stays here as the
|
|
12
|
+
// same .tbl the app already had.
|
|
13
|
+
//
|
|
14
|
+
// Facet counts and groups come from TanStack's models too, never from a second
|
|
15
|
+
// pass over the data. Grouping is the reader's, one column at a time, chosen
|
|
16
|
+
// from the columns that already offer a chip-set; a group is one full-width
|
|
17
|
+
// row, so there is nothing to aggregate. The grouped column is sorted first -
|
|
18
|
+
// derived here and never written back, so the URL still says only what the
|
|
19
|
+
// reader clicked.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Fragment,
|
|
23
|
+
useCallback,
|
|
24
|
+
useEffect,
|
|
25
|
+
useLayoutEffect,
|
|
26
|
+
useMemo,
|
|
27
|
+
useRef,
|
|
28
|
+
useState,
|
|
29
|
+
} from "react";
|
|
30
|
+
import { Link, useNavigate } from "react-router";
|
|
31
|
+
import { ChevronDown, ChevronRight, ChevronUp } from "lucide-react";
|
|
32
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
33
|
+
import {
|
|
34
|
+
columnFacetingFeature,
|
|
35
|
+
columnFilteringFeature,
|
|
36
|
+
columnGroupingFeature,
|
|
37
|
+
columnResizingFeature,
|
|
38
|
+
columnSizingFeature,
|
|
39
|
+
columnVisibilityFeature,
|
|
40
|
+
createExpandedRowModel,
|
|
41
|
+
createFacetedUniqueValues,
|
|
42
|
+
createFilteredRowModel,
|
|
43
|
+
createGroupedRowModel,
|
|
44
|
+
createSortedRowModel,
|
|
45
|
+
globalFilteringFeature,
|
|
46
|
+
rowExpandingFeature,
|
|
47
|
+
rowSortingFeature,
|
|
48
|
+
tableFeatures,
|
|
49
|
+
useTable,
|
|
50
|
+
} from "@tanstack/react-table";
|
|
51
|
+
import type { ColumnDef, GroupingState, Row, RowData } from "@tanstack/react-table";
|
|
52
|
+
import type { ReactNode } from "react";
|
|
53
|
+
import { useDensity } from "../app/density";
|
|
54
|
+
import { useToastStore } from "../app/toast";
|
|
55
|
+
import { useUiStore } from "../app/ui-store";
|
|
56
|
+
import { useSelectionStore } from "../selection/store";
|
|
57
|
+
import { toClipboard } from "../lib/clipboard";
|
|
58
|
+
import { comparatorFor } from "./compare";
|
|
59
|
+
import { defaultCell } from "./cells";
|
|
60
|
+
import { Toolbar } from "./Toolbar";
|
|
61
|
+
import type { ExportActions } from "./Toolbar";
|
|
62
|
+
import { csvFilename, toCsv, toExcelCsv, toMarkdown } from "./export";
|
|
63
|
+
import { facetGroupFrom } from "./facet-groups";
|
|
64
|
+
import type { FacetGroup } from "./facet-groups";
|
|
65
|
+
import {
|
|
66
|
+
activeGroup,
|
|
67
|
+
groupOptions,
|
|
68
|
+
groupingSorting,
|
|
69
|
+
leafRows,
|
|
70
|
+
stripeParity,
|
|
71
|
+
} from "./grouping";
|
|
72
|
+
import { applyUpdater, useTableState } from "./useTableState";
|
|
73
|
+
import type { SortEntry } from "./sort-url";
|
|
74
|
+
import type { CellValue, ColumnSpec } from "./types";
|
|
75
|
+
import { canFacet, cellText, defaultAlign, isTextish } from "./types";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Registered features, and only those: v9 installs nothing it is not asked for.
|
|
79
|
+
*
|
|
80
|
+
* No `facetedRowModel`: without it a column's unique values are counted over
|
|
81
|
+
* every row, so a chip's count is the whole table's and the set never changes
|
|
82
|
+
* shape under the reader. Registering `createFacetedRowModel()` here would
|
|
83
|
+
* make the counts follow the other filters and the search instead, and would
|
|
84
|
+
* finally reach the `count === 0` branch in Facets.tsx.
|
|
85
|
+
*
|
|
86
|
+
* No `rowAggregationFeature`: a group is drawn as one full-width row, so its
|
|
87
|
+
* other cells are never asked for a value.
|
|
88
|
+
*/
|
|
89
|
+
const FEATURES = tableFeatures({
|
|
90
|
+
rowSortingFeature,
|
|
91
|
+
sortedRowModel: createSortedRowModel(),
|
|
92
|
+
columnFilteringFeature,
|
|
93
|
+
globalFilteringFeature,
|
|
94
|
+
filteredRowModel: createFilteredRowModel(),
|
|
95
|
+
columnFacetingFeature,
|
|
96
|
+
facetedUniqueValues: createFacetedUniqueValues(),
|
|
97
|
+
columnGroupingFeature,
|
|
98
|
+
groupedRowModel: createGroupedRowModel(),
|
|
99
|
+
rowExpandingFeature,
|
|
100
|
+
expandedRowModel: createExpandedRowModel(),
|
|
101
|
+
columnVisibilityFeature,
|
|
102
|
+
columnSizingFeature,
|
|
103
|
+
columnResizingFeature,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
type Features = typeof FEATURES;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Above this many rows the body is virtualized. Never paginated: a page
|
|
110
|
+
* boundary breaks find-in-page, breaks "sort by date and look at the end",
|
|
111
|
+
* and turns one list into an argument about which page a row is on.
|
|
112
|
+
*/
|
|
113
|
+
const VIRTUALIZE_AT = 200;
|
|
114
|
+
|
|
115
|
+
/** Row heights, matching the two --row-h settings the density toggle writes. */
|
|
116
|
+
const ROW_H = { comfortable: 40, compact: 30 } as const;
|
|
117
|
+
|
|
118
|
+
/** How far a column may be auto-fitted before it is just a wide column. */
|
|
119
|
+
const AUTOFIT_MAX = 560;
|
|
120
|
+
const MIN_WIDTH = 56;
|
|
121
|
+
|
|
122
|
+
export interface DataTableProps<T extends RowData> {
|
|
123
|
+
/** Stable id. Widths and hidden columns are remembered under it. */
|
|
124
|
+
tableId: string;
|
|
125
|
+
columns: readonly ColumnSpec<T>[];
|
|
126
|
+
rows: readonly T[];
|
|
127
|
+
rowId: (row: T) => string;
|
|
128
|
+
/** The order the page declares. */
|
|
129
|
+
defaultSort?: SortEntry[];
|
|
130
|
+
/** Index pages: put the sort in the address so the view can be sent. */
|
|
131
|
+
sortInUrl?: boolean;
|
|
132
|
+
/** Where a row goes when it is clicked. Omitted where a row goes nowhere. */
|
|
133
|
+
rowLink?: (row: T) => string | null | undefined;
|
|
134
|
+
/** Copy and reveal, per row. */
|
|
135
|
+
rowActions?: (row: T) => ReactNode;
|
|
136
|
+
/** The catalog id this row is, so the global selection can light it up. */
|
|
137
|
+
selectionId?: (row: T) => string | null | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* A second row underneath, spanning every column. Returns null for the rows
|
|
140
|
+
* that have nothing to expand - which is most of them, most of the time.
|
|
141
|
+
* A table with detail rows is never virtualized: the rows are no longer one
|
|
142
|
+
* height, and a virtualizer that guesses wrong scrolls to the wrong place.
|
|
143
|
+
*/
|
|
144
|
+
subRow?: (row: T) => ReactNode | null;
|
|
145
|
+
/** Rows at which the toolbar appears. 8 on an index page, 10 in a README. */
|
|
146
|
+
toolbarAt?: number;
|
|
147
|
+
/** README tables: a text filter and nothing else. */
|
|
148
|
+
minimal?: boolean;
|
|
149
|
+
/** No toolbar, no sort, no memory. Small README tables stay quiet. */
|
|
150
|
+
plain?: boolean;
|
|
151
|
+
empty?: ReactNode;
|
|
152
|
+
/** Accessible name, and the name the export menu reports. */
|
|
153
|
+
caption?: string;
|
|
154
|
+
className?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The columns a menu may hide: everything but the one that names the row. */
|
|
158
|
+
function hideable<T>(spec: ColumnSpec<T>, primaryId: string): boolean {
|
|
159
|
+
return spec.enableHiding ?? spec.id !== primaryId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function DataTable<T extends RowData>({
|
|
163
|
+
tableId,
|
|
164
|
+
columns: specs,
|
|
165
|
+
rows: data,
|
|
166
|
+
rowId,
|
|
167
|
+
defaultSort = [],
|
|
168
|
+
sortInUrl = false,
|
|
169
|
+
rowLink,
|
|
170
|
+
rowActions,
|
|
171
|
+
selectionId,
|
|
172
|
+
subRow,
|
|
173
|
+
toolbarAt = 8,
|
|
174
|
+
minimal = false,
|
|
175
|
+
plain = false,
|
|
176
|
+
empty,
|
|
177
|
+
caption,
|
|
178
|
+
className = "",
|
|
179
|
+
}: DataTableProps<T>) {
|
|
180
|
+
const navigate = useNavigate();
|
|
181
|
+
const { density } = useDensity();
|
|
182
|
+
const zebra = useUiStore((s) => s.zebra);
|
|
183
|
+
const toggleZebra = useUiStore((s) => s.toggleZebra);
|
|
184
|
+
const selection = useSelectionStore((s) => s.selection);
|
|
185
|
+
const say = useToastStore((s) => s.say);
|
|
186
|
+
|
|
187
|
+
const tableRef = useRef<HTMLTableElement | null>(null);
|
|
188
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
189
|
+
|
|
190
|
+
const specById = useMemo(
|
|
191
|
+
() => new Map(specs.map((spec) => [spec.id, spec])),
|
|
192
|
+
[specs],
|
|
193
|
+
);
|
|
194
|
+
const columnIds = useMemo(() => specs.map((spec) => spec.id), [specs]);
|
|
195
|
+
// The column that names the row: it carries the link and the j/k focus, and
|
|
196
|
+
// it is the one column a reader may not switch off.
|
|
197
|
+
const primaryId = (specs.find((spec) => spec.primary) ?? specs[0])?.id ?? "";
|
|
198
|
+
|
|
199
|
+
const state = useTableState({
|
|
200
|
+
tableId,
|
|
201
|
+
columnIds,
|
|
202
|
+
defaultSort,
|
|
203
|
+
sortInUrl: sortInUrl && !plain,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Widths measured from the first layout. Transient on purpose: only a width
|
|
207
|
+
// the reader dragged is worth remembering, and a width measured in a narrow
|
|
208
|
+
// window should not follow them into a wide one.
|
|
209
|
+
const [measured, setMeasured] = useState<Record<string, number>>({});
|
|
210
|
+
const sizing = useMemo(
|
|
211
|
+
() => ({ ...measured, ...state.columnSizing }),
|
|
212
|
+
[measured, state.columnSizing],
|
|
213
|
+
);
|
|
214
|
+
const resized = Object.keys(state.columnSizing).length > 0;
|
|
215
|
+
|
|
216
|
+
const columnDefs = useMemo<ColumnDef<Features, T, unknown>[]>(
|
|
217
|
+
() =>
|
|
218
|
+
specs.map((spec) => {
|
|
219
|
+
const compare = comparatorFor(spec.type);
|
|
220
|
+
return {
|
|
221
|
+
id: spec.id,
|
|
222
|
+
accessorFn: (row: T) => spec.value(row),
|
|
223
|
+
header: spec.header,
|
|
224
|
+
// Ascending only. The table reverses this itself for descending.
|
|
225
|
+
sortFn: (a: Row<Features, T>, b: Row<Features, T>, id: string) =>
|
|
226
|
+
compare(a.getValue<CellValue>(id), b.getValue<CellValue>(id)),
|
|
227
|
+
// The empty cell goes last in both directions, which is a promise
|
|
228
|
+
// no comparator can keep on its own.
|
|
229
|
+
sortUndefined: "last" as const,
|
|
230
|
+
enableSorting: (spec.enableSorting ?? true) && !plain,
|
|
231
|
+
enableHiding: hideable(spec, primaryId),
|
|
232
|
+
// Foldable where there is a chip-set. Static on purpose: the other
|
|
233
|
+
// half of that - two or more values - is read off the table, which
|
|
234
|
+
// does not exist yet here, and a fold with one group is harmless.
|
|
235
|
+
enableGrouping: Boolean(spec.facet) && canFacet(spec.type) && !plain && !minimal,
|
|
236
|
+
// A chip-set narrows to the values it has switched on; an empty set
|
|
237
|
+
// is not a filter.
|
|
238
|
+
filterFn: (row: Row<Features, T>, id: string, value: unknown) => {
|
|
239
|
+
if (!Array.isArray(value) || value.length === 0) return true;
|
|
240
|
+
return (value as string[]).includes(cellText(row.getValue(id)));
|
|
241
|
+
},
|
|
242
|
+
size: spec.size,
|
|
243
|
+
minSize: spec.minSize ?? MIN_WIDTH,
|
|
244
|
+
};
|
|
245
|
+
}),
|
|
246
|
+
[specs, plain, minimal, primaryId],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// The columns that may fold the rows, and the one that does. A fold is only
|
|
250
|
+
// offered where the toolbar is: below that, there is nothing to fold.
|
|
251
|
+
const foldable = useMemo(
|
|
252
|
+
() =>
|
|
253
|
+
plain || minimal || data.length < toolbarAt
|
|
254
|
+
? []
|
|
255
|
+
: specs.filter((spec) => spec.facet && canFacet(spec.type)).map((spec) => spec.id),
|
|
256
|
+
[specs, plain, minimal, data.length, toolbarAt],
|
|
257
|
+
);
|
|
258
|
+
const groupBy = activeGroup(state.groupBy, foldable);
|
|
259
|
+
const grouping = useMemo<GroupingState>(() => (groupBy ? [groupBy] : []), [groupBy]);
|
|
260
|
+
// The grouped column sorted first, derived and never written back: the
|
|
261
|
+
// updater TanStack hands to setSorting is applied to the reader's own sort.
|
|
262
|
+
const sorting = useMemo(() => groupingSorting(state.sorting, groupBy), [state.sorting, groupBy]);
|
|
263
|
+
|
|
264
|
+
const table = useTable<Features, T>({
|
|
265
|
+
features: FEATURES,
|
|
266
|
+
columns: columnDefs,
|
|
267
|
+
data: data as T[],
|
|
268
|
+
getRowId: (row) => rowId(row),
|
|
269
|
+
state: {
|
|
270
|
+
sorting,
|
|
271
|
+
globalFilter: state.globalFilter,
|
|
272
|
+
columnFilters: state.columnFilters,
|
|
273
|
+
columnVisibility: state.columnVisibility,
|
|
274
|
+
columnSizing: sizing,
|
|
275
|
+
grouping,
|
|
276
|
+
expanded: state.expanded,
|
|
277
|
+
},
|
|
278
|
+
onSortingChange: state.setSorting,
|
|
279
|
+
onColumnFiltersChange: state.setColumnFilters,
|
|
280
|
+
onColumnVisibilityChange: state.setColumnVisibility,
|
|
281
|
+
onGroupingChange: (updater) => state.setGroupBy(applyUpdater(updater, grouping)[0] ?? null),
|
|
282
|
+
onExpandedChange: state.setExpanded,
|
|
283
|
+
// Columns stay where they are when one of them folds the rows; the fold
|
|
284
|
+
// is a row above them, not a column moved to the front.
|
|
285
|
+
groupedColumnMode: false,
|
|
286
|
+
enableGrouping: !plain && !minimal,
|
|
287
|
+
// TanStack would open every group again whenever a filter or the data
|
|
288
|
+
// changed; here a fold lasts until the grouping column changes.
|
|
289
|
+
autoResetExpanded: false,
|
|
290
|
+
onColumnSizingChange: (updater) => {
|
|
291
|
+
const next = applyUpdater(updater, sizing);
|
|
292
|
+
// Only what differs from the measured layout is the reader's doing, and
|
|
293
|
+
// only the reader's doing is worth storing.
|
|
294
|
+
state.setColumnSizing((previous) => {
|
|
295
|
+
const out = { ...previous };
|
|
296
|
+
for (const [id, width] of Object.entries(next)) {
|
|
297
|
+
if (measured[id] !== width) out[id] = width;
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
columnResizeMode: "onChange",
|
|
303
|
+
enableMultiSort: true,
|
|
304
|
+
enableSortingRemoval: true,
|
|
305
|
+
globalFilterFn: (row: Row<Features, T>, id: string, value: unknown) => {
|
|
306
|
+
const needle = String(value).trim().toLowerCase();
|
|
307
|
+
if (needle === "") return true;
|
|
308
|
+
return cellText(row.getValue<CellValue>(id))
|
|
309
|
+
.toLowerCase()
|
|
310
|
+
.includes(needle);
|
|
311
|
+
},
|
|
312
|
+
// The free-text filter searches what is written in words, not the numbers
|
|
313
|
+
// and dates beside them: typing "3" should not match a version column.
|
|
314
|
+
getColumnCanGlobalFilter: (column) => {
|
|
315
|
+
const spec = specById.get(column.id);
|
|
316
|
+
return spec !== undefined && isTextish(spec.type);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const visibleColumns = table.getVisibleLeafColumns();
|
|
321
|
+
const visibleIds = visibleColumns.map((column) => column.id).join(",");
|
|
322
|
+
// What is drawn: group rows with the leaves of the open ones beneath them.
|
|
323
|
+
const rows = table.getRowModel().rows;
|
|
324
|
+
// What the filter left: leaves only, folded or not.
|
|
325
|
+
const shown = table.getFilteredRowModel().rows.length;
|
|
326
|
+
const total = data.length;
|
|
327
|
+
|
|
328
|
+
// Measure once per column set, before paint, so the resize handles have a
|
|
329
|
+
// real width to start from and nothing is ever seen mid-settle.
|
|
330
|
+
useLayoutEffect(() => {
|
|
331
|
+
const element = tableRef.current;
|
|
332
|
+
if (!element) return;
|
|
333
|
+
const ids = visibleIds === "" ? [] : visibleIds.split(",");
|
|
334
|
+
const missing = ids.filter((id) => measured[id] === undefined);
|
|
335
|
+
if (missing.length === 0) return;
|
|
336
|
+
const next: Record<string, number> = {};
|
|
337
|
+
for (const id of missing) {
|
|
338
|
+
const th = element.querySelector<HTMLElement>(
|
|
339
|
+
`th[data-col="${CSS.escape(id)}"]`,
|
|
340
|
+
);
|
|
341
|
+
if (th) next[id] = Math.round(th.getBoundingClientRect().width);
|
|
342
|
+
}
|
|
343
|
+
if (Object.keys(next).length > 0) {
|
|
344
|
+
setMeasured((previous) => ({ ...previous, ...next }));
|
|
345
|
+
}
|
|
346
|
+
}, [visibleIds, measured]);
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Double-clicking an edge fits the column to what is in it. The cells are
|
|
350
|
+
* briefly let out of their box, read, and put back inside one frame, which
|
|
351
|
+
* is the only way to ask a clipped element how wide it would rather be.
|
|
352
|
+
*/
|
|
353
|
+
const autoFit = useCallback(
|
|
354
|
+
(columnId: string) => {
|
|
355
|
+
const element = tableRef.current;
|
|
356
|
+
if (!element) return;
|
|
357
|
+
element.classList.add("cell-measure");
|
|
358
|
+
let widest = MIN_WIDTH;
|
|
359
|
+
const cells = element.querySelectorAll<HTMLElement>(
|
|
360
|
+
`[data-col="${CSS.escape(columnId)}"] .cell-body`,
|
|
361
|
+
);
|
|
362
|
+
for (const cell of cells) widest = Math.max(widest, cell.scrollWidth);
|
|
363
|
+
element.classList.remove("cell-measure");
|
|
364
|
+
// The padding the cell itself adds, which scrollWidth does not include.
|
|
365
|
+
const width = Math.min(AUTOFIT_MAX, widest + 34);
|
|
366
|
+
state.setColumnSizing((previous) => ({ ...previous, [columnId]: width }));
|
|
367
|
+
},
|
|
368
|
+
[state.setColumnSizing],
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// --- The toolbar's inputs ------------------------------------------------
|
|
372
|
+
|
|
373
|
+
// TanStack counts a column's values once per data set; `data` is in the
|
|
374
|
+
// dependencies so the toolbar is rebuilt when it does.
|
|
375
|
+
const facetGroups = useMemo<FacetGroup[]>(() => {
|
|
376
|
+
if (plain || minimal) return [];
|
|
377
|
+
return specs.flatMap((spec) => {
|
|
378
|
+
const counts = table.getColumn(spec.id)?.getFacetedUniqueValues();
|
|
379
|
+
const group = counts ? facetGroupFrom(spec, counts, state.facetValues(spec.id)) : null;
|
|
380
|
+
return group ? [group] : [];
|
|
381
|
+
});
|
|
382
|
+
}, [table, specs, data, plain, minimal, state.facetValues]);
|
|
383
|
+
|
|
384
|
+
// Leaves only, in display order, folded or not: an export is the view, and
|
|
385
|
+
// a folded group is still in it.
|
|
386
|
+
const sheet = useCallback(() => {
|
|
387
|
+
const leaves = table.getVisibleLeafColumns();
|
|
388
|
+
return {
|
|
389
|
+
headers: leaves.map((column) => specById.get(column.id)?.header ?? column.id),
|
|
390
|
+
rows: leafRows(table.getSortedRowModel().rows).map((row) =>
|
|
391
|
+
leaves.map((column) => cellText(row.getValue<CellValue>(column.id))),
|
|
392
|
+
),
|
|
393
|
+
};
|
|
394
|
+
}, [table, rows, specById]);
|
|
395
|
+
|
|
396
|
+
const exports = useMemo<ExportActions>(
|
|
397
|
+
() => ({
|
|
398
|
+
copyMarkdown: () => void toClipboard(toMarkdown(sheet())).then((ok) => say(ok ? "Markdown table copied" : "could not reach the clipboard")),
|
|
399
|
+
copyCsv: () => void toClipboard(toCsv(sheet())).then((ok) => say(ok ? "CSV table copied" : "could not reach the clipboard")),
|
|
400
|
+
downloadCsv: () => {
|
|
401
|
+
const blob = new Blob([toExcelCsv(sheet())], {
|
|
402
|
+
type: "text/csv;charset=utf-8",
|
|
403
|
+
});
|
|
404
|
+
const url = URL.createObjectURL(blob);
|
|
405
|
+
const link = document.createElement("a");
|
|
406
|
+
link.href = url;
|
|
407
|
+
link.download = csvFilename(tableId);
|
|
408
|
+
document.body.append(link);
|
|
409
|
+
link.click();
|
|
410
|
+
link.remove();
|
|
411
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
412
|
+
say(`CSV downloaded — ${link.download}`);
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
[sheet, tableId, say],
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const columnToggles = useMemo(
|
|
419
|
+
() =>
|
|
420
|
+
table
|
|
421
|
+
.getAllLeafColumns()
|
|
422
|
+
.filter((column) => column.getCanHide())
|
|
423
|
+
.map((column) => ({
|
|
424
|
+
id: column.id,
|
|
425
|
+
header: specById.get(column.id)?.header ?? column.id,
|
|
426
|
+
visible: column.getIsVisible(),
|
|
427
|
+
})),
|
|
428
|
+
[table, specById, state.columnVisibility],
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const showToolbar = !plain && total >= toolbarAt;
|
|
432
|
+
|
|
433
|
+
// --- Rows ----------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
const virtual = rows.length > VIRTUALIZE_AT && !subRow;
|
|
436
|
+
const rowHeight = ROW_H[density];
|
|
437
|
+
const virtualizer = useVirtualizer({
|
|
438
|
+
count: virtual ? rows.length : 0,
|
|
439
|
+
getScrollElement: () => scrollRef.current,
|
|
440
|
+
estimateSize: () => rowHeight,
|
|
441
|
+
getItemKey: (index) => rows[index]?.id ?? index,
|
|
442
|
+
overscan: 12,
|
|
443
|
+
});
|
|
444
|
+
const items = virtual ? virtualizer.getVirtualItems() : [];
|
|
445
|
+
const padTop = items.length > 0 ? (items[0]?.start ?? 0) : 0;
|
|
446
|
+
const padBottom =
|
|
447
|
+
items.length > 0
|
|
448
|
+
? virtualizer.getTotalSize() - (items[items.length - 1]?.end ?? 0)
|
|
449
|
+
: 0;
|
|
450
|
+
|
|
451
|
+
const selectedRowId = useMemo(() => {
|
|
452
|
+
if (!selectionId || !selection) return null;
|
|
453
|
+
const hit = data.find((row) => selectionId(row) === selection.id);
|
|
454
|
+
return hit ? rowId(hit) : null;
|
|
455
|
+
}, [selectionId, selection, data, rowId]);
|
|
456
|
+
|
|
457
|
+
// A selection made somewhere else - the tree, a diagram, the palette - has
|
|
458
|
+
// to be visible here, which means scrolled to when it is off screen.
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
if (!selectedRowId) return;
|
|
461
|
+
const row = tableRef.current?.querySelector<HTMLElement>(
|
|
462
|
+
`tr[data-row="${CSS.escape(selectedRowId)}"]`,
|
|
463
|
+
);
|
|
464
|
+
row?.scrollIntoView({ block: "nearest" });
|
|
465
|
+
}, [selectedRowId]);
|
|
466
|
+
|
|
467
|
+
const columnCount = visibleColumns.length + (rowActions ? 1 : 0);
|
|
468
|
+
|
|
469
|
+
// Striping reads off the display position rather than nth-child, so a
|
|
470
|
+
// virtualized body's spacer rows cannot flip the parity; a group row has
|
|
471
|
+
// none, and the count starts again beneath it.
|
|
472
|
+
const parity = useMemo(() => stripeParity(rows.map((row) => row.getIsGrouped())), [rows]);
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A group: one row across every column, at the same height as the rows it
|
|
476
|
+
* holds so the virtualizer's arithmetic still stands. Its label is the
|
|
477
|
+
* column's own cell drawn over the first leaf - a facet column's cell
|
|
478
|
+
* renders its value and nothing else, which is what makes the first leaf a
|
|
479
|
+
* fair stand-in for the group. Not a j/k stop and not a row link: it is a
|
|
480
|
+
* heading, and the only thing it does is fold.
|
|
481
|
+
*/
|
|
482
|
+
const renderGroupRow = (row: Row<Features, T>) => {
|
|
483
|
+
const spec = row.groupingColumnId ? specById.get(row.groupingColumnId) : undefined;
|
|
484
|
+
const label = spec
|
|
485
|
+
? spec.cell
|
|
486
|
+
? spec.cell(row.original)
|
|
487
|
+
: defaultCell(spec.type, row.getValue<CellValue>(spec.id))
|
|
488
|
+
: null;
|
|
489
|
+
const open = row.getIsExpanded();
|
|
490
|
+
return (
|
|
491
|
+
<tr
|
|
492
|
+
key={row.id}
|
|
493
|
+
data-group={row.id}
|
|
494
|
+
className="tbl-group"
|
|
495
|
+
style={virtual ? { height: rowHeight } : undefined}
|
|
496
|
+
>
|
|
497
|
+
<td colSpan={columnCount} className="px-4 align-middle">
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
onClick={row.getToggleExpandedHandler()}
|
|
501
|
+
aria-expanded={open}
|
|
502
|
+
title={open ? "Fold this group" : "Open this group"}
|
|
503
|
+
className="mono flex max-w-full items-center gap-1.5 rounded-control text-muted hover:text-ink"
|
|
504
|
+
>
|
|
505
|
+
{open ? (
|
|
506
|
+
<ChevronDown size={12} aria-hidden className="shrink-0" />
|
|
507
|
+
) : (
|
|
508
|
+
<ChevronRight size={12} aria-hidden className="shrink-0" />
|
|
509
|
+
)}
|
|
510
|
+
<span className="min-w-0 truncate text-ink">{label}</span>
|
|
511
|
+
<span className="tnum">{row.subRows.length}</span>
|
|
512
|
+
</button>
|
|
513
|
+
</td>
|
|
514
|
+
</tr>
|
|
515
|
+
);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const renderRow = (row: Row<Features, T>, index: number) => {
|
|
519
|
+
if (row.getIsGrouped()) return renderGroupRow(row);
|
|
520
|
+
const original = row.original;
|
|
521
|
+
const link = rowLink?.(original) ?? null;
|
|
522
|
+
const active = selectedRowId !== null && row.id === selectedRowId;
|
|
523
|
+
const row_ = (
|
|
524
|
+
<tr
|
|
525
|
+
key={row.id}
|
|
526
|
+
data-row={row.id}
|
|
527
|
+
data-even={parity[index] ?? undefined}
|
|
528
|
+
data-selected={active || undefined}
|
|
529
|
+
style={virtual ? { height: rowHeight } : undefined}
|
|
530
|
+
className={active ? "bg-surface" : undefined}
|
|
531
|
+
onClick={
|
|
532
|
+
link
|
|
533
|
+
? (event) => {
|
|
534
|
+
// Ids, row actions and links inside cells are their own
|
|
535
|
+
// targets; the row is what is left over.
|
|
536
|
+
if (
|
|
537
|
+
(event.target as HTMLElement).closest("a,button,input,label")
|
|
538
|
+
) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
navigate(link);
|
|
542
|
+
}
|
|
543
|
+
: undefined
|
|
544
|
+
}
|
|
545
|
+
>
|
|
546
|
+
{row.getVisibleCells().map((cell) => {
|
|
547
|
+
const spec = specById.get(cell.column.id);
|
|
548
|
+
if (!spec) return null;
|
|
549
|
+
const value = cell.getValue<CellValue>();
|
|
550
|
+
const align = spec.align ?? defaultAlign(spec.type);
|
|
551
|
+
const body = spec.cell
|
|
552
|
+
? spec.cell(original)
|
|
553
|
+
: defaultCell(spec.type, value, spec.href?.(original));
|
|
554
|
+
const isPrimary = spec.id === primaryId;
|
|
555
|
+
return (
|
|
556
|
+
<td
|
|
557
|
+
key={cell.id}
|
|
558
|
+
data-col={spec.id}
|
|
559
|
+
title={spec.title?.(original)}
|
|
560
|
+
className={`px-4 align-middle ${align === "right" ? "text-right" : ""} ${
|
|
561
|
+
link && isPrimary ? "cursor-pointer" : ""
|
|
562
|
+
}`}
|
|
563
|
+
style={resized ? { width: sizing[spec.id] } : undefined}
|
|
564
|
+
>
|
|
565
|
+
<span
|
|
566
|
+
className={`cell-body ${spec.type === "text" ? "" : "cell-body-nowrap"} ${
|
|
567
|
+
align === "right" ? "text-right" : ""
|
|
568
|
+
}`}
|
|
569
|
+
>
|
|
570
|
+
{link && isPrimary ? (
|
|
571
|
+
// The row navigates on click; this is the same trip for a
|
|
572
|
+
// keyboard, and the link j/k walks. A real <Link>, so the
|
|
573
|
+
// basename is applied and a middle click still opens a tab.
|
|
574
|
+
<Link
|
|
575
|
+
to={link}
|
|
576
|
+
data-nav-item
|
|
577
|
+
className="rounded-control hover:underline"
|
|
578
|
+
>
|
|
579
|
+
{body}
|
|
580
|
+
</Link>
|
|
581
|
+
) : (
|
|
582
|
+
body
|
|
583
|
+
)}
|
|
584
|
+
</span>
|
|
585
|
+
</td>
|
|
586
|
+
);
|
|
587
|
+
})}
|
|
588
|
+
{rowActions ? (
|
|
589
|
+
<td className="px-4 align-middle whitespace-nowrap">
|
|
590
|
+
{rowActions(original)}
|
|
591
|
+
</td>
|
|
592
|
+
) : null}
|
|
593
|
+
</tr>
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const detail = subRow?.(original) ?? null;
|
|
597
|
+
if (!detail) return row_;
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<Fragment key={row.id}>
|
|
601
|
+
{row_}
|
|
602
|
+
<tr className="bg-surface">
|
|
603
|
+
<td colSpan={columnCount} className="px-4 py-2">
|
|
604
|
+
{detail}
|
|
605
|
+
</td>
|
|
606
|
+
</tr>
|
|
607
|
+
</Fragment>
|
|
608
|
+
);
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<div className={className}>
|
|
613
|
+
{showToolbar ? (
|
|
614
|
+
<Toolbar
|
|
615
|
+
filter={state.globalFilter}
|
|
616
|
+
onFilter={state.setGlobalFilter}
|
|
617
|
+
facets={facetGroups}
|
|
618
|
+
onToggleFacet={state.toggleFacet}
|
|
619
|
+
shown={shown}
|
|
620
|
+
total={total}
|
|
621
|
+
filtersActive={state.filtersActive}
|
|
622
|
+
onClearFilters={state.clearFilters}
|
|
623
|
+
/* Offered above five columns - counted as the table has, not as the
|
|
624
|
+
menu would list: the column that names the row is not hideable,
|
|
625
|
+
and a six-column table is still a six-column table. */
|
|
626
|
+
columns={!minimal && specs.length > 5 ? columnToggles : undefined}
|
|
627
|
+
onToggleColumn={
|
|
628
|
+
!minimal && specs.length > 5
|
|
629
|
+
? (id) => table.getColumn(id)?.toggleVisibility()
|
|
630
|
+
: undefined
|
|
631
|
+
}
|
|
632
|
+
groupBy={groupBy}
|
|
633
|
+
groupOptions={minimal ? undefined : groupOptions(facetGroups)}
|
|
634
|
+
onGroupBy={minimal ? undefined : state.setGroupBy}
|
|
635
|
+
/* A README gets a text filter and nothing else; striping is a
|
|
636
|
+
preference about the app, and a README is not where it is set. */
|
|
637
|
+
zebra={zebra}
|
|
638
|
+
onToggleZebra={minimal ? undefined : toggleZebra}
|
|
639
|
+
exports={minimal ? undefined : exports}
|
|
640
|
+
/>
|
|
641
|
+
) : null}
|
|
642
|
+
|
|
643
|
+
{/* `isolate`: the sticky head and column stack by z-index inside the
|
|
644
|
+
table, and those numbers must not reach the page - a corner cell at
|
|
645
|
+
z-30 would otherwise ride over the pinned name row as it passes. */}
|
|
646
|
+
<div
|
|
647
|
+
ref={scrollRef}
|
|
648
|
+
className="isolate overflow-x-auto rounded-card border border-line shadow-xs"
|
|
649
|
+
style={virtual ? { maxHeight: "70vh", overflowY: "auto" } : undefined}
|
|
650
|
+
>
|
|
651
|
+
<table
|
|
652
|
+
ref={tableRef}
|
|
653
|
+
data-nav-list
|
|
654
|
+
className={`tbl tbl-sticky ${zebra && !plain ? "tbl-zebra" : ""}`}
|
|
655
|
+
style={
|
|
656
|
+
resized
|
|
657
|
+
? { tableLayout: "fixed", width: "max-content", minWidth: "100%" }
|
|
658
|
+
: undefined
|
|
659
|
+
}
|
|
660
|
+
>
|
|
661
|
+
{caption ? <caption className="sr-only">{caption}</caption> : null}
|
|
662
|
+
<thead>
|
|
663
|
+
{table.getHeaderGroups().map((group) => (
|
|
664
|
+
<tr key={group.id} className="text-left">
|
|
665
|
+
{group.headers.map((header) => {
|
|
666
|
+
const spec = specById.get(header.column.id);
|
|
667
|
+
if (!spec) return null;
|
|
668
|
+
const align = spec.align ?? defaultAlign(spec.type);
|
|
669
|
+
// The direction is the table's, so a grouped column shows
|
|
670
|
+
// the way its groups run; the rank is the reader's, so their
|
|
671
|
+
// first key is not badged as the second.
|
|
672
|
+
const sorted = header.column.getIsSorted();
|
|
673
|
+
const canSort = header.column.getCanSort();
|
|
674
|
+
const rank = state.sorting.findIndex((entry) => entry.id === header.column.id);
|
|
675
|
+
return (
|
|
676
|
+
<th
|
|
677
|
+
key={header.id}
|
|
678
|
+
data-col={spec.id}
|
|
679
|
+
scope="col"
|
|
680
|
+
/* No `relative` here: the sticky header is already a
|
|
681
|
+
containing block for the resize handle, and a utility
|
|
682
|
+
`position` would take the header off the top of the
|
|
683
|
+
scroller it is supposed to be pinned to. */
|
|
684
|
+
className="px-4 font-normal"
|
|
685
|
+
style={resized ? { width: sizing[spec.id] } : undefined}
|
|
686
|
+
aria-sort={
|
|
687
|
+
sorted === "asc"
|
|
688
|
+
? "ascending"
|
|
689
|
+
: sorted === "desc"
|
|
690
|
+
? "descending"
|
|
691
|
+
: undefined
|
|
692
|
+
}
|
|
693
|
+
>
|
|
694
|
+
{canSort ? (
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
698
|
+
data-sorted={sorted !== false}
|
|
699
|
+
title={`Sort by ${spec.header}. Shift-click to add a second key.`}
|
|
700
|
+
className={`th-sort ${align === "right" ? "justify-end" : ""}`}
|
|
701
|
+
>
|
|
702
|
+
<span className="truncate">{spec.header}</span>
|
|
703
|
+
{sorted === "desc" ? (
|
|
704
|
+
<ChevronDown
|
|
705
|
+
size={12}
|
|
706
|
+
aria-hidden
|
|
707
|
+
className="sort-mark"
|
|
708
|
+
/>
|
|
709
|
+
) : (
|
|
710
|
+
<ChevronUp
|
|
711
|
+
size={12}
|
|
712
|
+
aria-hidden
|
|
713
|
+
className="sort-mark"
|
|
714
|
+
/>
|
|
715
|
+
)}
|
|
716
|
+
{/* Only worth saying when there is more than one key. */}
|
|
717
|
+
{rank > 0 ? (
|
|
718
|
+
<span className="sort-rank">{rank + 1}</span>
|
|
719
|
+
) : null}
|
|
720
|
+
</button>
|
|
721
|
+
) : (
|
|
722
|
+
<span className="label">{spec.header}</span>
|
|
723
|
+
)}
|
|
724
|
+
{!plain ? (
|
|
725
|
+
<span
|
|
726
|
+
/* A pointer affordance and nothing else. Given no
|
|
727
|
+
role: the app does not implement keyboard column
|
|
728
|
+
resizing, and a control that announces itself
|
|
729
|
+
without answering the keyboard is worse than one
|
|
730
|
+
that stays quiet. */
|
|
731
|
+
aria-hidden
|
|
732
|
+
title={`Drag to resize ${spec.header}, double-click to fit`}
|
|
733
|
+
className={`col-resize ${
|
|
734
|
+
header.column.getIsResizing() ? "col-resize-on" : ""
|
|
735
|
+
}`}
|
|
736
|
+
onMouseDown={header.getResizeHandler()}
|
|
737
|
+
onTouchStart={header.getResizeHandler()}
|
|
738
|
+
onDoubleClick={() => autoFit(spec.id)}
|
|
739
|
+
/>
|
|
740
|
+
) : null}
|
|
741
|
+
</th>
|
|
742
|
+
);
|
|
743
|
+
})}
|
|
744
|
+
{rowActions ? <th className="px-4" /> : null}
|
|
745
|
+
</tr>
|
|
746
|
+
))}
|
|
747
|
+
</thead>
|
|
748
|
+
<tbody>
|
|
749
|
+
{rows.length === 0 ? (
|
|
750
|
+
<tr>
|
|
751
|
+
<td colSpan={columnCount} className="px-4 text-muted">
|
|
752
|
+
{empty ?? "nothing here matches this filter"}
|
|
753
|
+
</td>
|
|
754
|
+
</tr>
|
|
755
|
+
) : virtual ? (
|
|
756
|
+
<>
|
|
757
|
+
{padTop > 0 ? (
|
|
758
|
+
<tr aria-hidden style={{ height: padTop }}>
|
|
759
|
+
<td colSpan={columnCount} />
|
|
760
|
+
</tr>
|
|
761
|
+
) : null}
|
|
762
|
+
{items.map((item) => {
|
|
763
|
+
const row = rows[item.index];
|
|
764
|
+
return row ? renderRow(row, item.index) : null;
|
|
765
|
+
})}
|
|
766
|
+
{padBottom > 0 ? (
|
|
767
|
+
<tr aria-hidden style={{ height: padBottom }}>
|
|
768
|
+
<td colSpan={columnCount} />
|
|
769
|
+
</tr>
|
|
770
|
+
) : null}
|
|
771
|
+
</>
|
|
772
|
+
) : (
|
|
773
|
+
rows.map((row, index) => renderRow(row, index))
|
|
774
|
+
)}
|
|
775
|
+
</tbody>
|
|
776
|
+
</table>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
);
|
|
780
|
+
}
|