diff --git a/.github/workflows/publish-staging-image-test.yml b/.github/workflows/publish-staging-image-test.yml new file mode 100644 index 000000000..3b7f9a299 --- /dev/null +++ b/.github/workflows/publish-staging-image-test.yml @@ -0,0 +1,140 @@ +name: Deploy to STAGING (LocalStack Test) + +permissions: + id-token: write # Required for OIDC authentication + contents: read # Standard permission for GitHub Actions + +on: + push: + branches: + - develop + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy-staging: + runs-on: ubuntu-latest + env: + ENVIRONMENT: staging + IMAGE_NAME: ${{ vars.PUBLICECR_URI || 'localhost.localstack.cloud:4510/core-web-app' }} + # LocalStack configuration + AWS_ENDPOINT_URL: http://localhost:4566 + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + + services: + localstack: + image: localstack/localstack:latest + ports: + - 4566:4566 + env: + SERVICES: s3,ecr,ecs,cloudfront + DEBUG: 1 + DOCKER_HOST: unix:///var/run/docker.sock + # Remove the volumes section when running under act + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract commit SHA + id: extract_version + run: | + COMMIT_SHA=$(echo ${{ github.sha }} | cut -c1-8) + echo "version=${COMMIT_SHA}" >> $GITHUB_OUTPUT + echo "Using version: ${COMMIT_SHA}" + + - name: Build a Docker image + run: | + docker buildx build \ + --build-arg DEPLOYMENT_ENV=staging \ + --build-arg CORE_WEB_APP_VERSION=${{ steps.extract_version.outputs.version }} \ + -t core-web-app:staging . + + - name: Extract static assets from Docker image + run: | + rm -rf ./assets-for-s3 + mkdir -p ./assets-for-s3/${{ steps.extract_version.outputs.version }}/public + mkdir -p ./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next + docker create --name assets-extractor core-web-app:staging + docker cp assets-extractor:/app/public/. \ + ./assets-for-s3/${{ steps.extract_version.outputs.version }}/public/ + docker cp assets-extractor:/app/.next/static/. \ + ./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next/static/ + docker rm assets-extractor + + - name: Install AWS CLI + run: | + apt-get update + apt-get install -y awscli + + - name: Setup LocalStack S3 and sync assets + run: | + # Create S3 bucket in LocalStack + aws --endpoint-url=http://localhost:4566 s3 mb s3://staging-test-bucket || true + + # upload extracted static assets to LocalStack S3 with versioned paths + aws --endpoint-url=http://localhost:4566 s3 sync \ + ./assets-for-s3/${{ steps.extract_version.outputs.version }}/public \ + s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/public \ + --cache-control "public, max-age=31536000, immutable" + + aws --endpoint-url=http://localhost:4566 s3 sync \ + ./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next/static \ + s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/_next/static \ + --cache-control "public, max-age=31536000, immutable" + + - name: Create LocalStack CloudFront distribution + run: | + # Create a CloudFront distribution in LocalStack (basic setup) + aws --endpoint-url=http://localhost:4566 cloudfront create-distribution \ + --distribution-config '{ + "CallerReference": "test-distribution-'${{ steps.extract_version.outputs.version }}'", + "Comment": "Test distribution for version '${{ steps.extract_version.outputs.version }}'", + "DefaultRootObject": "index.html", + "Origins": { + "Quantity": 1, + "Items": [{ + "Id": "s3-origin", + "DomainName": "staging-test-bucket.s3.localhost.localstack.cloud", + "S3OriginConfig": { + "OriginAccessIdentity": "" + } + }] + }, + "DefaultCacheBehavior": { + "TargetOriginId": "s3-origin", + "ViewerProtocolPolicy": "allow-all", + "MinTTL": 0, + "ForwardedValues": { + "QueryString": false, + "Cookies": {"Forward": "none"} + } + }, + "Enabled": true + }' || echo "CloudFront distribution creation skipped" + + - name: List CloudFront distributions + run: | + aws --endpoint-url=http://localhost:4566 cloudfront list-distributions + + - name: Verify deployment + run: | + echo "Deployment verification:" + echo "- Version: ${{ steps.extract_version.outputs.version }}" + echo "- S3 assets uploaded to: s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/" + echo "- Docker image: localhost.localstack.cloud:4510/core-web-app:staging" + + # List S3 objects + echo "S3 bucket contents:" + aws --endpoint-url=http://localhost:4566 s3 ls s3://staging-test-bucket/ --recursive + + # Check ECS service status + echo "ECS service status:" + aws --endpoint-url=http://localhost:4566 ecs describe-services \ + --cluster test-staging-cluster \ + --services test-staging-service || echo "ECS service check skipped" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ab0f64a24 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +services: + web: + container_name: core-web-app + build: + context: . + dockerfile: Dockerfile + args: + DEPLOYMENT_ENV: staging + NEXT_PUBLIC_ACCOUNTING_BASE_URL: ${NEXT_PUBLIC_ACCOUNTING_BASE_URL} + NEXT_PUBLIC_BLUE_NAAS_URL: ${NEXT_PUBLIC_BLUE_NAAS_URL} + NEXT_PUBLIC_SMALL_SCALE_SIMULATOR_URL: ${NEXT_PUBLIC_SMALL_SCALE_SIMULATOR_URL} + NEXT_PUBLIC_CELL_SVC_BASE_URL: ${NEXT_PUBLIC_CELL_SVC_BASE_URL} + NEXT_PUBLIC_THUMBNAIL_GENERATION_BASE_URL: ${NEXT_PUBLIC_THUMBNAIL_GENERATION_BASE_URL} + NEXT_PUBLIC_ME_MODEL_ANALYSIS_WS_URL: ${NEXT_PUBLIC_ME_MODEL_ANALYSIS_WS_URL} + NEXT_PUBLIC_VIRTUAL_LAB_API_URL: ${NEXT_PUBLIC_VIRTUAL_LAB_API_URL} + NEXT_PUBLIC_NOTEBOOK_SERVICE_BASE_URL: ${NEXT_PUBLIC_NOTEBOOK_SERVICE_BASE_URL} + NEXT_PUBLIC_OBI_ONE_URL: ${NEXT_PUBLIC_OBI_ONE_URL} + NEXT_PUBLIC_MATOMO_URL: ${NEXT_PUBLIC_MATOMO_URL} + NEXT_PUBLIC_MATOMO_CDN_URL: ${NEXT_PUBLIC_MATOMO_CDN_URL} + NEXT_PUBLIC_MATOMO_SITE_ID: ${NEXT_PUBLIC_MATOMO_SITE_ID} + NEXT_PUBLIC_DEPLOYMENT_ENV: ${NEXT_PUBLIC_DEPLOYMENT_ENV} + PRIMARY_HOSTNAME: ${PRIMARY_HOSTNAME} + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY} + NEXT_PUBLIC_SANITY_DATASET: ${NEXT_PUBLIC_SANITY_DATASET} + NEXT_PUBLIC_AI_AGENT_URL: ${NEXT_PUBLIC_AI_AGENT_URL} + NEXT_PUBLIC_ENABLE_RUN_NOTEBOOK: ${NEXT_PUBLIC_ENABLE_RUN_NOTEBOOK} + NEXT_PUBLIC_ENTITY_CORE_URL: ${NEXT_PUBLIC_ENTITY_CORE_URL} + NEXT_PUBLIC_ENTITY_CORE_PUBLIC_VIRTUAL_LAB_ID: ${NEXT_PUBLIC_ENTITY_CORE_PUBLIC_VIRTUAL_LAB_ID} + NEXT_PUBLIC_ENTITY_CORE_PUBLIC_PROJECT_ID: ${NEXT_PUBLIC_ENTITY_CORE_PUBLIC_PROJECT_ID} + NEXT_PUBLIC_DEFAULT_BRAIN_REGION_HIERARCHY_ID: ${NEXT_PUBLIC_DEFAULT_BRAIN_REGION_HIERARCHY_ID} + NEXT_PUBLIC_DEFAULT_SELECTED_BRAIN_REGION_ID: ${NEXT_PUBLIC_DEFAULT_SELECTED_BRAIN_REGION_ID} + NEXT_PUBLIC_BASIC_CELL_GROUPS_AND_REGIONS_BRAIN_REGION_ANNOTATION_VALUE: ${NEXT_PUBLIC_BASIC_CELL_GROUPS_AND_REGIONS_BRAIN_REGION_ANNOTATION_VALUE} + NEXT_PUBLIC_DEFAULT_BRAIN_ATLAS_ID: ${NEXT_PUBLIC_DEFAULT_BRAIN_ATLAS_ID} + NEXT_PUBLIC_ROOT_BRAIN_REGION_ID: ${NEXT_PUBLIC_ROOT_BRAIN_REGION_ID} + NEXT_PUBLIC_ROOT_BRAIN_REGION_ANNOTATION_VALUE: ${NEXT_PUBLIC_ROOT_BRAIN_REGION_ANNOTATION_VALUE} + NEXT_PUBLIC_LEGACY_DEFAULT_CIRCUIT_ID: ${NEXT_PUBLIC_LEGACY_DEFAULT_CIRCUIT_ID} + NEXT_PUBLIC_CELL_COMPOSITION_ID: ${NEXT_PUBLIC_CELL_COMPOSITION_ID} + KEYCLOAK_ISSUER: ${KEYCLOAK_ISSUER} + KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + MAILCHIMP_API_KEY: ${MAILCHIMP_API_KEY} + MAILCHIMP_AUDIENCE_ID: ${MAILCHIMP_AUDIENCE_ID} + MAILCHIMP_API_SERVER: ${MAILCHIMP_API_SERVER} + + ports: + - '3000:8000' + env_file: + - .env.local + restart: unless-stopped diff --git a/package.json b/package.json index 2b01f2c0f..fa92ac744 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "input-otp": "^1.4.2", "install": "^0.13.0", "jotai": "^2.7.2", + "jotai-cache": "^0.5.0", "jotai-devtools": "^0.11.0", "jotai-optics": "^0.4.0", "js-yaml": "^4.1.0", @@ -119,6 +120,7 @@ "next-sanity-client": "^1.0.8", "nuqs": "^2.4.1", "optics-ts": "^2.4.0", + "p-map": "^7.0.3", "pako": "^2.1.0", "performant-array-to-tree": "^1.11.0", "plotly.js-dist-min": "^2.35.3", @@ -128,6 +130,7 @@ "react-confetti": "^6.4.0", "react-dom": "19.1.0", "react-error-boundary": "^5.0.0", + "react-hotkeys-hook": "^5.1.0", "react-intersection-observer": "^9.5.2", "react-ipynb-renderer": "^2.2.4", "react-markdown": "^9.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c98cc29fb..2a0836d8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,6 +242,9 @@ importers: jotai: specifier: ^2.7.2 version: 2.12.2(@types/react@19.1.0)(react@19.1.0) + jotai-cache: + specifier: ^0.5.0 + version: 0.5.0(jotai@2.12.2(@types/react@19.1.0)(react@19.1.0)) jotai-devtools: specifier: ^0.11.0 version: 0.11.0(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1) @@ -287,6 +290,9 @@ importers: optics-ts: specifier: ^2.4.0 version: 2.4.1 + p-map: + specifier: ^7.0.3 + version: 7.0.3 pako: specifier: ^2.1.0 version: 2.1.0 @@ -314,6 +320,9 @@ importers: react-error-boundary: specifier: ^5.0.0 version: 5.0.0(react@19.1.0) + react-hotkeys-hook: + specifier: ^5.1.0 + version: 5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-intersection-observer: specifier: ^9.5.2 version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -8324,6 +8333,11 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jotai-cache@0.5.0: + resolution: {integrity: sha512-29pUuEfSXL7Ba6lxZmiNDARc73TspWzAzCy0jCkk2uEOnFJ6kaUBZTp/AZSwnIsh1ndfUfM9/QpbLU7uJAQL0A==} + peerDependencies: + jotai: '>=2.0.0' + jotai-devtools@0.11.0: resolution: {integrity: sha512-UVjjG7EHG/oIm1CKN46xwIbQDLW4Wkpq6pm0mjcIWATvs/W9r52U5l2FW36d9QWIRash+ccJ7RXgiz1JLPGmpA==} engines: {node: '>=14.0.0'} @@ -10325,6 +10339,12 @@ packages: '@types/react': optional: true + react-hotkeys-hook@5.1.0: + resolution: {integrity: sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-i18next@14.0.2: resolution: {integrity: sha512-YOB/H1IgXveEWeTsCHez18QjDXImzVZOcF9/JroSbjYoN1LOfCoARFJUQQ8VNow0TnGOtHq9SwTmismm78CTTA==} peerDependencies: @@ -21517,6 +21537,10 @@ snapshots: jose@4.15.9: {} + jotai-cache@0.5.0(jotai@2.12.2(@types/react@19.1.0)(react@19.1.0)): + dependencies: + jotai: 2.12.2(@types/react@19.1.0)(react@19.1.0) + jotai-devtools@0.11.0(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1): dependencies: '@mantine/code-highlight': 7.17.4(@mantine/core@7.17.4(@mantine/hooks@7.17.4(react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@7.17.4(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -23905,6 +23929,11 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + react-hotkeys-hook@5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-i18next@14.0.2(i18next@23.16.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 diff --git a/src/api/entitycore/queries/assets/index.ts b/src/api/entitycore/queries/assets/index.ts index 59c50ea4b..284489b1d 100644 --- a/src/api/entitycore/queries/assets/index.ts +++ b/src/api/entitycore/queries/assets/index.ts @@ -4,9 +4,15 @@ import authApiClient from '@/api/apiClient'; import { EntityTypeValue } from '@/api/entitycore/types/entity-type'; import { getEntityCoreContext } from '@/api/entitycore/utils'; +import { compactRecord } from '@/utils/dictionary'; import { entityCoreUrl } from '@/config'; -import type { AssetLabel, EntityCoreDataType, IAsset } from '@/api/entitycore/types/shared/global'; +import type { + AssetLabel, + DirectoryListContent, + EntityCoreDataType, + IAsset, +} from '@/api/entitycore/types/shared/global'; import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response'; import type { WorkspaceContext } from '@/types/common'; @@ -67,6 +73,7 @@ export async function downloadAsset(params: { id: string; asRawResponse: true; retryOnError?: boolean; + signal?: AbortSignal; }): Promise; export async function downloadAsset(params: { @@ -77,6 +84,7 @@ export async function downloadAsset(params: { id: string; asRawResponse?: false; retryOnError?: boolean; + signal?: AbortSignal; }): Promise; /** @@ -96,6 +104,7 @@ export async function downloadAsset({ asRawResponse = false, retryOnError = false, assetPath = '', + signal, }: { ctx?: WorkspaceContext; entityType: EntityTypeValue; @@ -104,13 +113,15 @@ export async function downloadAsset({ id: string; asRawResponse?: boolean; retryOnError?: boolean; + signal?: AbortSignal; }): Promise { const api = await authApiClient(entityCoreUrl); return await api.get( `/${kebabCase(entityType)}/${entityId}/assets/${id}/download`, { ...getEntityCoreContext(ctx), - queryParams: { asset_path: assetPath }, + queryParams: compactRecord({ asset_path: assetPath }), + signal, }, { asRawResponse, retryOnError } ); @@ -169,3 +180,39 @@ export async function createJsonAsset({ body: formData, }); } + +/** + * Lists the contents of a directory of assets by its id from the EntityCoreAPI. + * + * @param {Object} params - The parameters object + * @param {string} params.entityType - The type of the entity to retrieve + * @param {string} params.entityId - The id of the entity to retrieve + * @param {string} params.id - The id of the asset to retrieve + * @returns {Promise} A promise that resolves to the response from the API + */ +export async function listDirectoryOfAssets({ + ctx, + entityType, + entityId, + id, + retryOnError = false, +}: { + ctx?: WorkspaceContext; + entityType: EntityTypeValue; + entityId: string; + id: string; + retryOnError?: boolean; +}): Promise { + const api = await authApiClient(entityCoreUrl); + return await api.get( + `/${kebabCase(entityType)}/${entityId}/assets/${id}/list`, + { + headers: { + ...getEntityCoreContext(ctx).headers, + accept: 'application/json', + 'content-type': 'application/json', + }, + }, + { retryOnError } + ); +} diff --git a/src/api/entitycore/queries/general/publication.ts b/src/api/entitycore/queries/general/publication.ts new file mode 100644 index 000000000..942a83336 --- /dev/null +++ b/src/api/entitycore/queries/general/publication.ts @@ -0,0 +1,56 @@ +import { entityCoreApi, getEntityCoreContext } from '@/api/entitycore/utils'; + +import type { IPublication, IPublicationFilter } from '@/api/entitycore/types/entities/publication'; +import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response'; +import type { WorkspaceContext } from '@/types/common'; + +const baseUri = '/publication'; + +/** + * Retrieves publications from the EntityCore API. + * + * @param withFacets - Optional flag to include facet information in the response. + * @param filters - Filter criteria for querying publications. + * @param context - Workspace context containing authentication and environment details. + * @returns A promise resolving to the EntityCore response containing publications. + */ +export async function getPublications({ + withFacets, + filters, + context, +}: { + withFacets?: boolean; + filters: IPublicationFilter; + context: WorkspaceContext; +}) { + const api = await entityCoreApi(); + return await api.get>(baseUri, { + queryParams: { + ...filters, + with_facets: withFacets, + }, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...getEntityCoreContext(context).headers, + }, + }); +} + +/** + * Retrieves a publication by its ID. + * + * @param id - The unique identifier of the publication. + * @param context - The workspace context containing authentication and request metadata. + * @returns A promise that resolves to the publication object. + */ +export async function getPublication({ id, context }: { id: string; context: WorkspaceContext }) { + const api = await entityCoreApi(); + return await api.get(`${baseUri}/${id}`, { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...getEntityCoreContext(context).headers, + }, + }); +} diff --git a/src/api/entitycore/queries/general/scientific-artifact-publication-link.ts b/src/api/entitycore/queries/general/scientific-artifact-publication-link.ts new file mode 100644 index 000000000..9188b916c --- /dev/null +++ b/src/api/entitycore/queries/general/scientific-artifact-publication-link.ts @@ -0,0 +1,66 @@ +import { entityCoreApi, getEntityCoreContext } from '@/api/entitycore/utils'; + +import type { + IScientificArtifactPublicationLink, + IScientificArtifactPublicationLinkFilter, +} from '@/api/entitycore/types/entities/scientific-artifact-publication-link'; +import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response'; +import type { WorkspaceContext } from '@/types/common'; +import { compactRecord } from '@/utils/dictionary'; + +const baseUri = '/scientific-artifact-publication-link'; + +/** + * Retrieves scientific artifact publication links from the EntityCore API. + * + * @param withFacets - Optional flag to include facet information in the response. + * @param filters - Filter criteria for querying scientific artifact publication links. + * @param context - Workspace context containing authentication and environment details. + * @returns A promise resolving to the EntityCore response containing scientific artifact publication links. + */ +export async function getScientificArtifactPublicationLinks({ + withFacets, + filters, + context, +}: { + withFacets?: boolean; + filters: Partial; + context: WorkspaceContext; +}) { + const api = await entityCoreApi(); + return await api.get>(baseUri, { + queryParams: compactRecord({ + ...filters, + with_facets: withFacets, + }), + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...getEntityCoreContext(context).headers, + }, + }); +} + +/** + * Retrieves a scientific artifact publication link by its ID. + * + * @param id - The unique identifier of the scientific artifact publication link. + * @param context - The workspace context containing authentication and request metadata. + * @returns A promise that resolves to the scientific artifact publication link object. + */ +export async function getScientificArtifactPublicationLink({ + id, + context, +}: { + id: string; + context: WorkspaceContext; +}) { + const api = await entityCoreApi(); + return await api.get(`${baseUri}/${id}`, { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...getEntityCoreContext(context).headers, + }, + }); +} diff --git a/src/api/entitycore/queries/model/circuit.ts b/src/api/entitycore/queries/model/circuit.ts index 39ac45548..1bf9c456a 100644 --- a/src/api/entitycore/queries/model/circuit.ts +++ b/src/api/entitycore/queries/model/circuit.ts @@ -2,6 +2,8 @@ import { entityCoreApi, getEntityCoreContext } from '@/api/entitycore/utils'; import { compactRecord } from '@/utils/dictionary'; import type { ICircuit, ICircuitFilter } from '@/api/entitycore/types/entities/circuit'; +import type { HierarchyTreeResponse } from '@/api/entitycore/types/shared/hierarchy'; +import type { TDerivationType } from '@/api/entitycore/types/entities/derivation'; import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response'; import type { WorkspaceContext } from '@/types/common'; @@ -69,3 +71,40 @@ export async function getCircuit({ }, }); } + +/** + * Retrieves the hierarchy tree based on the specified derivation type. + * + ** Return a hierarchy tree of circuits based on derivations. + * Depending on the derivation type, the hierarchy will be built differently. In particular, a circuit is considered a root if it has no parents of the specified derivation type. + * The hierarchy assumes the following rules for the derivations: + ** A circuit can have zero or more children linked with any derivation type. + ** A circuit can have zero or more parents, provided each parent is different, and is linked with a different derivation type. + ** A public circuit can have any combination of public and private circuits as children. + ** A private circuit can have only private circuits with the same project_id as children. + * + ** See also https://github.com/openbraininstitute/entitycore/issues/292#issuecomment-3174884561 + * + * @param context - Optional workspace context for the API request. + * @param derivation_type - The type of derivation to filter the hierarchy. + * @returns A promise that resolves to the hierarchy tree response. + */ +export async function getCircuitHierarchyByDerivation({ + context, + derivation_type, +}: { + context?: WorkspaceContext | null; + derivation_type: TDerivationType; +}): Promise { + const api = await entityCoreApi(); + return await api.get(`${baseUri}/hierarchy`, { + queryParams: { + derivation_type, + }, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...getEntityCoreContext(context).headers, + }, + }); +} diff --git a/src/api/entitycore/types/entities/circuit.ts b/src/api/entitycore/types/entities/circuit.ts index 8aedee57c..874838793 100644 --- a/src/api/entitycore/types/entities/circuit.ts +++ b/src/api/entitycore/types/entities/circuit.ts @@ -69,6 +69,9 @@ interface CircuitBase { build_category: TCircuitBuildCategoryDictionary; scale: TCircuitScaleDictionary; root_circuit_id: string; + experiment_date: string | null; + contact_email: string | null; + published_in: string | null; } export interface ICircuit @@ -90,3 +93,58 @@ export interface ICircuitFilter SharedFilter, PaginationFilter, CircuitScaleFilter {} + +export type SonataCircuitNetworkEdgeConfigItem = { + edges_file: string; + populations: Record< + string, + { + type: string; + } + >; +}; + +export type SonataCircuitNetworkNodeConfigItem = { + nodes_file: string; + populations: Record< + string, + { + type: 'biophysical' | 'virtual'; + biophysical_neuron_models_dir?: string; + morphologies_dir?: string; + alternate_morphologies?: Record; + } + >; +}; + +export type SonataCircuitConfigNetworks = { + edges: Array; + nodes: Array; +}; + +export type SonataCircuitComponentConfig = { + biophysical_neuron_models_dir: string; + mechanisms_dir: string; + morphologies_dir: string; + point_neuron_models_dir: string; + provenance: { + id_mapping: string; + }; + synaptic_models_dir: string; + templates_dir: string; +}; + +export type ICircuitSonataConfiguration = { + components: SonataCircuitComponentConfig; + networks: SonataCircuitConfigNetworks; + node_sets_file: string; + version: number; + manifest: { + [key: string]: string; + }; +}; + +export type CircuitConnectivityMatricesConfiguration = Record< + string, + Record +>; diff --git a/src/api/entitycore/types/entities/derivation.ts b/src/api/entitycore/types/entities/derivation.ts index 6dcd6545c..cb604766a 100644 --- a/src/api/entitycore/types/entities/derivation.ts +++ b/src/api/entitycore/types/entities/derivation.ts @@ -1,5 +1,23 @@ import type { EntityCoreIdentifiable, EntityCoreType } from '@/api/entitycore/types/shared/global'; -import type { PaginationFilter, SharedFilter } from '@/api/entitycore/types/shared/request'; +import type { + EntityCoreTypeFilter, + PaginationFilter, + SharedFilter, +} from '@/api/entitycore/types/shared/request'; export interface IDerivationBase extends EntityCoreIdentifiable, EntityCoreType {} -export interface IDerivationFilter extends PaginationFilter, SharedFilter {} +export interface IDerivationFilter extends PaginationFilter, SharedFilter, EntityCoreTypeFilter {} + +export const DerivationType = { + /** + * Indicates that the entity was derived by extracting a set of nodes from a circuit. + */ + circuit_extraction: 'circuit_extraction', + + /** + * Indicates that the entity was derived by rewiring the connectivity of a circuit. + */ + circuit_rewiring: 'circuit_rewiring', +} as const; + +export type TDerivationType = (typeof DerivationType)[keyof typeof DerivationType]; diff --git a/src/api/entitycore/types/entities/publication.ts b/src/api/entitycore/types/entities/publication.ts new file mode 100644 index 000000000..1f94b2443 --- /dev/null +++ b/src/api/entitycore/types/entities/publication.ts @@ -0,0 +1,53 @@ +import { + EntityAuthorization, + EntityCoreIdentifiable, + EntityCoreOwnership, + EntityCoreType, + IContributor, + Timestamps, +} from '../shared/global'; +import { + ContributionFilter, + IdFilter, + NameFilter, + PaginationFilter, + SearchFilter, + TimestampsFilter, +} from '../shared/request'; + +export type Author = { + given_name: string; + family_name: string; +}; + +export interface PublicationBase + extends EntityCoreIdentifiable, + Timestamps, + EntityAuthorization, + EntityCoreType, + EntityCoreOwnership {} + +export interface IPublication extends PublicationBase { + contributions?: Array | null; + name: string; + description: string; + DOI: string | null; + title: string | null; + authors: Array | null; + publication_year: number | null; + abstract: string | null; +} + +export interface IPublicationFilter + extends NameFilter, + IdFilter, + TimestampsFilter, + ContributionFilter, + PaginationFilter, + SearchFilter { + DOI: string | null; + publication_year?: number | null; + publication_year__in?: number[] | null; + publication_year__lte?: number | null; + publication_year__gte?: number | null; +} diff --git a/src/api/entitycore/types/entities/scientific-artifact-publication-link.ts b/src/api/entitycore/types/entities/scientific-artifact-publication-link.ts new file mode 100644 index 000000000..ea8f8342e --- /dev/null +++ b/src/api/entitycore/types/entities/scientific-artifact-publication-link.ts @@ -0,0 +1,85 @@ +import type { ScientificArtifactBase } from '@/api/entitycore/types/entities/scientific-artifact'; +import type { IPublication } from '@/api/entitycore/types/entities/publication'; +import type { + EntityCoreIdentifiable, + EntityCoreOwnership, +} from '@/api/entitycore/types/shared/global'; +import type { + IdFilter, + NameFilter, + OwnershipFilter, + PaginationFilter, + SearchFilter, + TimestampsFilter, +} from '@/api/entitycore/types/shared/request'; + +export const PublicationType = { + EntitySource: { + key: 'entity_source', + label: 'Entity source', + }, + ComponentSource: { + key: 'component_source', + label: 'Component source', + }, + Application: { + key: 'application', + label: 'Application', + }, +} as const; + +export const PublicationTypeDictionary = Object.fromEntries( + Object.entries(PublicationType).map(([name, value]) => [name, value.key]) +) as { + [K in keyof typeof PublicationType]: (typeof PublicationType)[K]['key']; +}; + +export type TPublicationTypeDictionary = + (typeof PublicationTypeDictionary)[keyof typeof PublicationTypeDictionary]; + +type ScientificArtifactPublicationLinkBase = { + publication_type: TPublicationTypeDictionary; +}; + +export interface IScientificArtifactPublicationLink + extends EntityCoreIdentifiable, + EntityCoreOwnership, + ScientificArtifactPublicationLinkBase { + publication: IPublication; + scientific_artifact: ScientificArtifactBase; +} + +export interface IScientificArtifactPublicationLinkFilter + extends NameFilter, + IdFilter, + TimestampsFilter, + OwnershipFilter, + PaginationFilter, + SearchFilter { + DOI: string | null; + publication_year?: number | null; + publication_year__in?: number[] | null; + publication_year__lte?: number | null; + publication_year__gte?: number | null; + publication__name?: string | null; + publication__name__in?: string[] | null; + publication__name__ilike?: string | null; + + publication__id?: string | null; + publication__id__in?: string[] | null; + + publication__DOI?: string | null; + publication_type?: TPublicationTypeDictionary; + publication__publication_year?: number | null; + publication__publication_year_in?: number[] | null; + publication__publication_year__lte?: number | null; + publication__publication_year__gte?: number | null; + + scientific_artifact__id?: string | null; + scientific_artifact__id__in?: string[] | null; + + scientific_artifact__experiment_date__lte?: string | null; + scientific_artifact__experiment_date__gte?: string | null; + + scientific_artifact__contact_id?: string | null; +} diff --git a/src/api/entitycore/types/entities/scientific-artifact.ts b/src/api/entitycore/types/entities/scientific-artifact.ts new file mode 100644 index 000000000..d356865f9 --- /dev/null +++ b/src/api/entitycore/types/entities/scientific-artifact.ts @@ -0,0 +1,12 @@ +import { + EntityCoreIdentifiable, + EntityCoreType, + Timestamps, +} from '@/api/entitycore/types/shared/global'; + +export interface ScientificArtifactBase extends EntityCoreIdentifiable, Timestamps, EntityCoreType { + experiment_date: Date | null; + contact_email: string | null; + published_in: string | null; + atlas_id: string | null; +} diff --git a/src/api/entitycore/types/shared/global.ts b/src/api/entitycore/types/shared/global.ts index bd4dd24c7..7e9dcdd70 100644 --- a/src/api/entitycore/types/shared/global.ts +++ b/src/api/entitycore/types/shared/global.ts @@ -165,6 +165,12 @@ export enum AssetLabel { validation_result_figure = 'validation_result_figure', voltage_report = 'voltage_report', voxel_densities = 'voxel_densities', + network_stats_a = 'network_stats_a', + circuit_connectivity_matrices = 'circuit_connectivity_matrices', + node_stats = 'node_stats', + network_stats_b = 'network_stats_b', + circuit_visualization = 'circuit_visualization', + compressed_sonata_circuit = 'compressed_sonata_circuit', } type AssetBase = { @@ -222,3 +228,12 @@ export interface IEType extends IAnnotation {} // linear_density: '1/μm', // volume_density: '1/mm³', // } as const; + +export type DirectoryItem = { + name: string; + size: number; + last_modified: string; // as Date +}; +export type DirectoryListContent = { + files: Record; +}; diff --git a/src/api/entitycore/types/shared/hierarchy.ts b/src/api/entitycore/types/shared/hierarchy.ts new file mode 100644 index 000000000..cef7e30a7 --- /dev/null +++ b/src/api/entitycore/types/shared/hierarchy.ts @@ -0,0 +1,15 @@ +import { TDerivationType } from '@/api/entitycore/types/entities/derivation'; + +export interface HierarchyNode { + id: string; + name: string; + parent_id: string | null; + children: Array; + authorized_public: boolean; + authorized_project_id: string; +} + +export interface HierarchyTreeResponse { + derivation_type: TDerivationType; + data: Array; +} diff --git a/src/api/entitycore/types/shared/request.ts b/src/api/entitycore/types/shared/request.ts index 4b56deda7..67a6473bc 100644 --- a/src/api/entitycore/types/shared/request.ts +++ b/src/api/entitycore/types/shared/request.ts @@ -1,3 +1,5 @@ +import type { EntityTypeValue } from '@/api/entitycore/types/entity-type'; + export type TimestampsFilter = { creation_date__lte: Date | null; creation_date__gte: Date | null; @@ -48,9 +50,10 @@ export type IdFilter = Partial<{ id__in: string | Array; }>; -type SearchFilter = { +export type SearchFilter = { search: string | null; }; + export type NameFilter = { name: string | null; name__ilike: string | null; @@ -121,3 +124,7 @@ export interface IEntityFilter OwnershipFilter, TimestampsFilter, ContributionFilter {} + +export type EntityCoreTypeFilter = { + type: EntityTypeValue; +}; diff --git a/src/api/entitycore/types/shared/response.ts b/src/api/entitycore/types/shared/response.ts index d32ede0dd..443f97b0d 100644 --- a/src/api/entitycore/types/shared/response.ts +++ b/src/api/entitycore/types/shared/response.ts @@ -1,4 +1,4 @@ -interface Pagination { +export interface EntityCorePagination { page: number; page_size: number; total_items: number; @@ -14,6 +14,6 @@ type Facet = { export type Facets = Record>; export interface EntityCoreResponse { data: Array; - pagination: Pagination; + pagination: EntityCorePagination; facets?: Facets; } diff --git a/src/app/app/virtual-lab/(free)/explore/(interactive)/interactive/(data)/model/[type]/page.tsx b/src/app/app/virtual-lab/(free)/explore/(interactive)/interactive/(data)/model/[type]/page.tsx index 4822b6dc0..80d566b60 100644 --- a/src/app/app/virtual-lab/(free)/explore/(interactive)/interactive/(data)/model/[type]/page.tsx +++ b/src/app/app/virtual-lab/(free)/explore/(interactive)/interactive/(data)/model/[type]/page.tsx @@ -3,6 +3,7 @@ import omit from 'lodash/omit'; import ListingView from '@/features/views/listing/model-listing-view'; import { getEntityBySlug } from '@/entity-configuration/domain/helpers'; +import { tempCheckCircuitInDev } from '@/temp-circuit-check'; import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common'; import type { ModelEntitySlugValue } from '@/entity-configuration/domain/slug'; @@ -16,7 +17,9 @@ export default async function Page({ null >) { const params = await promisedParams; - const entity = getEntityBySlug({ slug: params.type }); + const type = tempCheckCircuitInDev(params.type); + + const entity = getEntityBySlug({ slug: type }); if (!entity) { notFound(); } diff --git a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/[id]/page.tsx b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/[id]/page.tsx index 65e24848a..1d4368534 100644 --- a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/[id]/page.tsx +++ b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/[id]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; -import DetailView from '@/features/views/details/model'; +import DetailView from '@/features/views/details/model'; import { getEntityBySlug } from '@/entity-configuration/domain/helpers'; import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common'; @@ -17,7 +17,7 @@ export const generateMetadata = async ({ params }: Props): Promise => return { title: data?.title ?? 'Model Details', - description: `discover ${data?.title ? `${data.title} details` : 'model details'}`, + description: `discover ${data?.title ? `${data.title} details` : 'discover model details'}`, }; }; diff --git a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/page.tsx b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/page.tsx index a405db8e4..7790ff01c 100644 --- a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/page.tsx +++ b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/[type]/page.tsx @@ -3,6 +3,7 @@ import omit from 'lodash/omit'; import ListingView from '@/features/views/listing/model-listing-view'; import { getEntityBySlug } from '@/entity-configuration/domain/helpers'; +import { tempCheckCircuitInDev } from '@/temp-circuit-check'; import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common'; import type { ModelEntitySlugValue } from '@/entity-configuration/domain/slug'; @@ -16,10 +17,13 @@ export default async function Page({ null >) { const params = await promisedParams; - const entity = getEntityBySlug({ slug: params.type }); + const type = tempCheckCircuitInDev(params.type); + + const entity = getEntityBySlug({ slug: type }); if (!entity) { notFound(); } + return ( + {children} + + ); +} diff --git a/src/components/build-section/cell-model-assignment/e-model/EModelView/exemplar-morphology.tsx b/src/components/build-section/cell-model-assignment/e-model/EModelView/exemplar-morphology.tsx index 17b495d2e..ed4627d36 100644 --- a/src/components/build-section/cell-model-assignment/e-model/EModelView/exemplar-morphology.tsx +++ b/src/components/build-section/cell-model-assignment/e-model/EModelView/exemplar-morphology.tsx @@ -1,3 +1,5 @@ +import isString from 'lodash/isString'; + import type { ColumnsType } from 'antd/es/table'; import DefaultEModelTable from '@/components/build-section/cell-model-assignment/e-model/EModelView/DefaultEModelTable'; @@ -26,7 +28,7 @@ const defaultColumnsFields = getFieldsDefinition([ const defaultColumns: ColumnsType = Object.entries( defaultColumnsFields ).map(([key, field]) => ({ - title: field.title.toUpperCase(), + title: isString(field.title) ? field.title.toUpperCase() : field.title, key, render: field.render, })); diff --git a/src/components/buttons/toolbar/index.tsx b/src/components/buttons/toolbar/index.tsx new file mode 100644 index 000000000..229f48985 --- /dev/null +++ b/src/components/buttons/toolbar/index.tsx @@ -0,0 +1,21 @@ +import { JSX, ReactNode } from 'react'; + +export function ToolbarButton({ + icon, + children, + stateIcon, +}: { + icon: JSX.Element; + stateIcon?: JSX.Element; + children: ReactNode; +}) { + return ( +
+
{children}
+
+ {stateIcon ?? icon} +
+
+
+ ); +} diff --git a/src/components/entities-type-stats/helpers.ts b/src/components/entities-type-stats/helpers.ts index 1f3c3537f..23695abd3 100644 --- a/src/components/entities-type-stats/helpers.ts +++ b/src/components/entities-type-stats/helpers.ts @@ -1,12 +1,12 @@ import { ReconstructionMorphology } from '@/entity-configuration/domain/experimental/reconstruction-morphology'; import { ElectricalCellRecording } from '@/entity-configuration/domain/experimental/electrical-cell-recording'; +import { SynapsePerConnection } from '@/entity-configuration/domain/experimental/synapse-per-connection'; +import { SingleNeuronSynaptome } from '@/entity-configuration/domain/model/single-neuron-synaptome'; import { NeuronDensity } from '@/entity-configuration/domain/experimental/neuron-density'; import { BoutonDensity } from '@/entity-configuration/domain/experimental/bouton-density'; -import { SynapsePerConnection } from '@/entity-configuration/domain/experimental/synapse-per-connection'; import { Emodel } from '@/entity-configuration/domain/model/e-model'; import { MEmodel } from '@/entity-configuration/domain/model/me-model'; -import { SingleNeuronSynaptome } from '@/entity-configuration/domain/model/single-neuron-synaptome'; -// import { Circuit } from '@/entity-configuration/domain/model/circuit'; +import { Circuit } from '@/entity-configuration/domain/model/circuit'; export const ExperimentalEntitiesTileTypes = { ReconstructionMorphology, @@ -20,4 +20,5 @@ export const ModelEntitiesTileTypes = { Emodel, MEmodel, SingleNeuronSynaptome, + Circuit, } as const; diff --git a/src/components/entities-type-stats/interactive-navigation-menu.tsx b/src/components/entities-type-stats/interactive-navigation-menu.tsx index 9346c16fc..a8fb6e136 100644 --- a/src/components/entities-type-stats/interactive-navigation-menu.tsx +++ b/src/components/entities-type-stats/interactive-navigation-menu.tsx @@ -6,11 +6,12 @@ import { useAtomValue } from 'jotai'; import { match } from 'ts-pattern'; import get from 'lodash/get'; -import { useFilteredCircuits } from '../explore-section/Circuit/ListView/ExploreCircuitTable'; import { dataTabAtom } from '@/components/explore-section/ExploreInteractive/interactive/entity-group-tab'; +import { useFilteredCircuits } from '@/components/explore-section/Circuit/ListView/ExploreCircuitTable'; import { useBrainRegionHierarchy } from '@/features/brain-region-hierarchy/context'; import { EntityTypeCount } from '@/components/entities-type-stats/stat-item'; import { entitiesCountAtom } from '@/services/entitycore/entities-count'; +import { tempIsCircuitInDev } from '@/temp-circuit-check'; import { ExperimentalEntitiesTileTypes, ModelEntitiesTileTypes, @@ -45,7 +46,6 @@ function EntityTypeStats(props: StatsPanelProps) { const pathName = usePathname(); const selectedTab = useAtomValue(dataTabAtom); const { error: circuitError, filteredCircuits } = useFilteredCircuits({ dataKey: props.dataKey }); - let data: EntityCountResponse | null = null; let error: Error | null = null; let isLoading = false; @@ -114,15 +114,17 @@ function EntityTypeStats(props: StatsPanelProps) { /> ); })} - + {!tempIsCircuitInDev() && ( + + )} )) .otherwise(() => null); diff --git a/src/components/explore-section/ExploreListingLayout/navigation-menu.tsx b/src/components/entities-type-stats/listing-navigation-menu.tsx similarity index 100% rename from src/components/explore-section/ExploreListingLayout/navigation-menu.tsx rename to src/components/entities-type-stats/listing-navigation-menu.tsx diff --git a/src/components/explore-section/Circuit/DetailView/header/Heading.tsx b/src/components/explore-section/Circuit/DetailView/header/Heading.tsx index 17c92c2ae..2206fba3c 100644 --- a/src/components/explore-section/Circuit/DetailView/header/Heading.tsx +++ b/src/components/explore-section/Circuit/DetailView/header/Heading.tsx @@ -116,7 +116,7 @@ export default function Heading({ content }: { content: CircuitSchemaProps }) { )} - +
diff --git a/src/components/explore-section/Circuit/global/Columns.tsx b/src/components/explore-section/Circuit/global/Columns.tsx index 3ac8d9dcc..81cf82fe0 100644 --- a/src/components/explore-section/Circuit/global/Columns.tsx +++ b/src/components/explore-section/Circuit/global/Columns.tsx @@ -32,7 +32,7 @@ const columns = ( { title: ( - + ), key: 'download', @@ -45,7 +45,7 @@ const columns = ( aria-label="Open download modal" onClick={() => handleOpenDownloadModal(record)} > - + ); }, diff --git a/src/components/explore-section/Circuit/global/ViewToggle.tsx b/src/components/explore-section/Circuit/global/ViewToggle.tsx index 163cd91cc..384f13b3f 100644 --- a/src/components/explore-section/Circuit/global/ViewToggle.tsx +++ b/src/components/explore-section/Circuit/global/ViewToggle.tsx @@ -1,18 +1,16 @@ import { Tooltip } from 'antd'; +import { useAtom } from 'jotai'; import { FlatListViewIcon, HierarchicalViewIcon } from '@/components/icons'; import { classNames } from '@/util/utils'; +import { queryParamsPerEntityTypeAtomFamily } from '@/state/explore-section/list-view-atoms'; + +export default function ViewToggle({ dataKey }: { dataKey: string }) { + const [queryParams, setQueryParams] = useAtom(queryParamsPerEntityTypeAtomFamily(dataKey)); + const toggle = queryParams?.view || 'flat'; -export default function ViewToggle({ - toggle, - setToggle, -}: { - toggle: 'hierarchical' | 'flat'; - setToggle: (toggle: 'hierarchical' | 'flat') => void; -}) { const handleViewChange = () => { - const newView = toggle === 'hierarchical' ? 'flat' : 'hierarchical'; - setToggle(newView); + setQueryParams({ ...queryParams, view: toggle === 'hierarchy' ? 'flat' : 'hierarchy' }); }; return ( @@ -22,7 +20,7 @@ export default function ViewToggle({
@@ -37,7 +35,7 @@ export default function ViewToggle({
diff --git a/src/components/explore-section/Circuit/global/circuit-table.tsx b/src/components/explore-section/Circuit/global/circuit-table.tsx index 250e61871..c2d655371 100644 --- a/src/components/explore-section/Circuit/global/circuit-table.tsx +++ b/src/components/explore-section/Circuit/global/circuit-table.tsx @@ -37,7 +37,7 @@ export default function CircuitTable({ const [, setFilter] = useAtom(setFilterAtom); // VIEWS - const [toggle, setToggle] = useState<'hierarchical' | 'flat'>('hierarchical'); + const [toggle] = useState<'hierarchical' | 'flat'>('hierarchical'); // SCROLL BEHAVIOR const [isAtStart, setIsAtStart] = useState(true); @@ -361,7 +361,7 @@ export default function CircuitTable({ numberOfFilters={Object.values(filters).filter((f) => f !== null).length} numberOfActiveColumns={columnState.filter((col) => col.isActive).length} /> - +
)} diff --git a/src/components/explore-section/Circuit/global/download/download-container.tsx b/src/components/explore-section/Circuit/global/download/download-container.tsx index d2a8e728a..55a67faaa 100644 --- a/src/components/explore-section/Circuit/global/download/download-container.tsx +++ b/src/components/explore-section/Circuit/global/download/download-container.tsx @@ -44,7 +44,7 @@ function FullCircuitItem({ content }: { content: DownloadItemProps }) { className="border-primary-6 flex h-7 w-7 items-center justify-center border border-solid" aria-label="Download the full circuit" > - + diff --git a/src/components/explore-section/Circuit/global/download/download-item.tsx b/src/components/explore-section/Circuit/global/download/download-item.tsx index 390690642..5ee4cc371 100644 --- a/src/components/explore-section/Circuit/global/download/download-item.tsx +++ b/src/components/explore-section/Circuit/global/download/download-item.tsx @@ -37,7 +37,7 @@ function DownloadChildrenItem({ className="border-primary-6 flex h-7 w-7 items-center justify-center border border-solid" aria-label={`Add download ${childrenItem.name} to the cart`} > - + diff --git a/src/components/explore-section/ExploreListingLayout/index.tsx b/src/components/explore-section/ExploreListingLayout/index.tsx index 3e42d4fb5..fb533e488 100644 --- a/src/components/explore-section/ExploreListingLayout/index.tsx +++ b/src/components/explore-section/ExploreListingLayout/index.tsx @@ -10,37 +10,37 @@ import { useAtomValue } from 'jotai'; import { useQueryState } from 'nuqs'; import get from 'lodash/get'; +import { useFilteredCircuits } from '../Circuit/ListView/ExploreCircuitTable'; import BackToInteractiveExplorationBtn from '@/components/explore-section/BackToInteractiveExplorationBtn'; -import NavigationMenu from '@/components/explore-section/ExploreListingLayout/navigation-menu'; +import NavigationMenu from '@/components/entities-type-stats/listing-navigation-menu'; import SimpleErrorComponent from '@/components/GenericErrorFallback'; -import { useFilteredCircuits } from '@/components/explore-section/Circuit/ListView/ExploreCircuitTable'; import { brainRegionBasicCellGroupsRegionsHierarchyAtom, DEFAULT_BRAIN_REGION_QUERY_ID, } from '@/features/brain-region-hierarchy/context'; import { userJourneyTracker } from '@/components/explore-section/Literature/user-journey'; -import { StatError } from '@/components/explore-section/ExploreInteractive/StatItem'; import { DataTypeGroup } from '@/entity-configuration/definitions/view-defs/types'; import { useCurrentExplorerArtifact } from '@/state/explore-section/artifact'; import { getEntityBySlug } from '@/entity-configuration/domain/helpers'; -import { resolveDataKey } from '@/utils/key-builder'; import { ensureString } from '@/util/type-guards'; import { ExperimentalEntitiesTileTypes, ModelEntitiesTileTypes, } from '@/components/entities-type-stats/helpers'; +import { classNames } from '@/util/utils'; -import type { NavigationMenuItem } from '@/components/explore-section/ExploreListingLayout/navigation-menu'; +import type { NavigationMenuItem } from '@/components/entities-type-stats/listing-navigation-menu'; import type { EntityCoreTypeConfig } from '@/entity-configuration/domain/types'; import type { EntitySlugValue } from '@/entity-configuration/domain/slug'; import type { WorkspaceContext } from '@/types/common'; +import { resolveDataKey } from '@/utils/key-builder'; +import { tempIsCircuitInDev } from '@/temp-circuit-check'; export default function ExploreListingLayout({ children }: { children: ReactNode }) { const router = useRouter(); const params = useParams(); const pathname = usePathname(); - const dataKey = resolveDataKey({ projectId: params.projectId, section: 'explore' }); const [brainRegionId] = useQueryState(DEFAULT_BRAIN_REGION_QUERY_ID); const brainRegionHierarchy = useAtomValue( useMemo(() => unwrap(brainRegionBasicCellGroupsRegionsHierarchyAtom), []) @@ -60,7 +60,6 @@ export default function ExploreListingLayout({ children }: { children: ReactNode ? ExperimentalEntitiesTileTypes : ModelEntitiesTileTypes; - const showCircuitMenu = dataTypeGroup === DataTypeGroup.ModelData; const activePath = pathname?.split('/').pop() || 'morphology'; const onClick: MenuProps['onClick'] = async (info) => { @@ -82,37 +81,42 @@ export default function ExploreListingLayout({ children }: { children: ReactNode router.push(key); }; - const nMenuItems = Object.keys(config).length + (showCircuitMenu ? 1 : 0); + const nMenuItems = Object.keys(config).length; const menuItemWidth = `${Math.floor(100 / nMenuItems) - 0.04}%`; - const items: Array = Object.keys(config).map((dataType) => { - const entity = get(config, `${dataType}`) as EntityCoreTypeConfig; - const key = entity?.slug!; - const active = entity?.slug === activePath; - const label = entity?.title!; - const entitytype = entity.type; - - return { - key, - entitytype, - title: label, - label, - className: 'text-center font-semibold', - style: { - backgroundColor: active ? 'white' : '#002766', - color: active ? '#002766' : 'white', - flexBasis: menuItemWidth, - }, - }; - }); - - const { filteredCircuits, loading, error } = useFilteredCircuits({ dataKey }); + const items: Array = Object.keys(config) + .map((dataType) => { + const entity = get(config, `${dataType}`) as EntityCoreTypeConfig; + const key = entity?.slug!; + const active = entity?.slug === activePath; + const label = entity?.title!; + const entitytype = entity.type; + + return { + key, + entitytype, + title: label, + label, + className: 'text-center font-semibold', + style: { + backgroundColor: active ? 'white' : '#002766', + color: active ? '#002766' : 'white', + flexBasis: menuItemWidth, + }, + }; + }) + .filter((item) => { + if (!tempIsCircuitInDev()) { + return item.key !== 'circuit'; + } + return true; + }); - if (error) { - return ; - } + const showCircuitMenu = dataTypeGroup === DataTypeGroup.ModelData; - if (showCircuitMenu && !loading) { + const dataKey = resolveDataKey({ projectId: params.projectId, section: 'explore' }); + const { filteredCircuits } = useFilteredCircuits({ dataKey }); + if (showCircuitMenu && !tempIsCircuitInDev()) { const circuitActive = activePath === 'circuit'; items.push({ @@ -147,7 +151,6 @@ export default function ExploreListingLayout({ children }: { children: ReactNode key={`${params.type}/${brainRegionId}`} > -
li]:gap2 [&>li]:flex [&>li]:h-[46px] [&>li]:items-center [&>li]:justify-center [&>li]:text-center' + )} items={items.map((p) => ({ ...p, itemIcon: }))} /> } diff --git a/src/components/explore-section/ExploreSectionListingView/ExploreSectionTable.tsx b/src/components/explore-section/ExploreSectionListingView/ExploreSectionTable.tsx index 3ad31e222..472d81357 100644 --- a/src/components/explore-section/ExploreSectionListingView/ExploreSectionTable.tsx +++ b/src/components/explore-section/ExploreSectionListingView/ExploreSectionTable.tsx @@ -9,6 +9,10 @@ import type { ExpandableConfig, RowSelectionType } from 'antd/es/table/interface import type { TableRef } from 'antd/es/table'; import LoadMoreButton from '@/components/explore-section/ExploreSectionListingView/LoadMoreButton'; +import TableControls from '@/components/listing-table/controls'; +import useResizeObserver from '@/hooks/useResizeObserver'; +import useScrollComplete from '@/hooks/useScrollComplete'; + import useRowSelection, { RenderButtonProps, } from '@/components/explore-section/ExploreSectionListingView/useRowSelection'; @@ -20,10 +24,6 @@ import { ExploreDataScope } from '@/types/explore-section/application'; import { DataType } from '@/constants/explore-section/list-views'; import { classNames } from '@/util/utils'; -import TableControls from '@/components/listing-table/controls'; -import useResizeObserver from '@/hooks/useResizeObserver'; -import useScrollComplete from '@/hooks/useScrollComplete'; - import type { EntityCoreIdentifiable } from '@/api/entitycore/types/shared/global'; import type { WorkspaceContext } from '@/types/common'; @@ -36,34 +36,64 @@ function CustomTH({ style, onClick, handleResizing, + className, ...props }: { children: ReactNode; style: CSSProperties; onClick: () => void; handleResizing: () => void; + className?: string; }) { + const { position, left, right, zIndex, transform } = style; + + // preserve positioning styles for fixed columns, but use our custom styles for everything else const modifiedStyle: CSSProperties = { - ...style, + // keep positioning styles for fixed columns + ...(position && { position }), + ...(left !== undefined && { left }), + ...(right !== undefined && { right }), + ...(zIndex !== undefined && { zIndex }), + ...(transform && { transform }), + fontWeight: '500', color: '#434343', verticalAlign: 'baseline', boxSizing: 'border-box', backgroundColor: 'white', + // force text wrapping with high priority + whiteSpace: 'normal !important' as any, + wordWrap: 'break-word !important' as any, + wordBreak: 'break-word !important' as any, + overflowWrap: 'break-word !important' as any, }; + // preserve the original className (which includes Ant Design's fixed column classes) + // and only add our custom class that doesn't interfere with positioning + const combinedClassName = classNames( + className, + 'before:content-none!', + // force text wrapping with high specificity + '[&>*]:whitespace-normal! [&>*]:break-words!' + ); + return handleResizing ? ( -
+