From 9c3d8a5a9140f880d675ce2c110fc8775293fb74 Mon Sep 17 00:00:00 2001 From: bilalesi Date: Mon, 21 Jul 2025 12:37:54 +0200 Subject: [PATCH 01/14] add paired neurons model and simulations with small-microcircuit --- .../types/entities/circuit-simulation-campaign.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/entitycore/types/entities/circuit-simulation-campaign.ts b/src/api/entitycore/types/entities/circuit-simulation-campaign.ts index c25e1475e..72a94d89f 100644 --- a/src/api/entitycore/types/entities/circuit-simulation-campaign.ts +++ b/src/api/entitycore/types/entities/circuit-simulation-campaign.ts @@ -13,9 +13,15 @@ import type { NameFilter, IEntityFilter, } from '@/api/entitycore/types/shared/request'; +<<<<<<< HEAD import type { TCircuitBuildCategoryDictionary, TCircuitScaleDictionary, +======= +import { + CircuitBuildCategoryDictionary, + CircuitScaleDictionary, +>>>>>>> 77c656e33 (add paired neurons model and simulations with small-microcircuit) } from '@/api/entitycore/types/entities/circuit'; interface ISimulationBase extends EntityCoreIdentifiable { From d7aedf05078a873422959f52c103762a48766e02 Mon Sep 17 00:00:00 2001 From: bilalesi Date: Wed, 16 Jul 2025 12:22:51 +0200 Subject: [PATCH 02/14] move circuit to entitycore part 1 (listing table and filters) --- .../interactive/(data)/model/[type]/page.tsx | 1 + .../{circuit => __circuit}/[id]/page.tsx | 0 .../model/{circuit => __circuit}/layout.tsx | 0 .../model/{circuit => __circuit}/page.tsx | 0 src/components/entities-type-stats/helpers.ts | 7 +- .../interactive-navigation-menu.tsx | 10 -- .../listing-navigation-menu.tsx} | 0 .../ExploreListingLayout/index.tsx | 36 +----- src/components/icons/Arrows-horizantal.tsx | 18 +++ .../definitions/fields-defs/common.tsx | 2 +- .../definitions/fields-defs/enums.ts | 1 + .../definitions/fields-defs/model.tsx | 88 ++++++++++++++- src/entity-configuration/definitions/types.ts | 9 +- src/entity-configuration/domain/helpers.ts | 28 ++++- src/entity-configuration/domain/index.ts | 2 - .../domain/model/circuit.ts | 3 +- .../domain/model/index.ts | 2 - .../filter-as-dropdown.tsx | 104 ++++++++++++++++++ .../listing-filter-panel.tsx | 53 +++++---- src/features/listing-filter-panel/util.ts | 13 ++- .../listing-filter-panel/value-range.tsx | 81 ++++++++++++++ .../views/listing/model-listing-view.tsx | 8 +- .../explore-section/column-key-to-filter.ts | 7 ++ 23 files changed, 392 insertions(+), 81 deletions(-) rename src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/{circuit => __circuit}/[id]/page.tsx (100%) rename src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/{circuit => __circuit}/layout.tsx (100%) rename src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/(data)/model/{circuit => __circuit}/page.tsx (100%) rename src/components/{explore-section/ExploreListingLayout/navigation-menu.tsx => entities-type-stats/listing-navigation-menu.tsx} (100%) create mode 100644 src/components/icons/Arrows-horizantal.tsx create mode 100644 src/features/listing-filter-panel/filter-as-dropdown.tsx create mode 100644 src/features/listing-filter-panel/value-range.tsx 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..cef6651cf 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 @@ -20,6 +20,7 @@ export default async function Page({ if (!entity) { notFound(); } + return ( ); })} - )) .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/ExploreListingLayout/index.tsx b/src/components/explore-section/ExploreListingLayout/index.tsx index 3e42d4fb5..f0fc8af0a 100644 --- a/src/components/explore-section/ExploreListingLayout/index.tsx +++ b/src/components/explore-section/ExploreListingLayout/index.tsx @@ -11,27 +11,24 @@ import { useQueryState } from 'nuqs'; import get from 'lodash/get'; 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 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'; @@ -40,7 +37,6 @@ 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 +56,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,7 +77,7 @@ 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) => { @@ -106,30 +101,6 @@ export default function ExploreListingLayout({ children }: { children: ReactNode }; }); - const { filteredCircuits, loading, error } = useFilteredCircuits({ dataKey }); - - if (error) { - return ; - } - - if (showCircuitMenu && !loading) { - const circuitActive = activePath === 'circuit'; - - items.push({ - key: 'circuit', - title: 'Circuit', - // @ts-expect-error - entitytype: 'Circuit', - label: `Circuit (${filteredCircuits.count})`, - className: 'text-center font-semibold', - style: { - backgroundColor: circuitActive ? 'white' : '#002766', - color: circuitActive ? '#002766' : 'white', - flexBasis: menuItemWidth, - }, - }); - } - // NOTE: this is legacy to handle details page, // TODO: (this should change to layout per page type (one for listing and one for details)) // ! The menu is not rendered for details pages (where the route contains `id` segment) @@ -147,7 +118,6 @@ export default function ExploreListingLayout({ children }: { children: ReactNode key={`${params.type}/${brainRegionId}`} > -
) { + return ( + + + + ); +} diff --git a/src/entity-configuration/definitions/fields-defs/common.tsx b/src/entity-configuration/definitions/fields-defs/common.tsx index a38fb4687..24e9097e6 100644 --- a/src/entity-configuration/definitions/fields-defs/common.tsx +++ b/src/entity-configuration/definitions/fields-defs/common.tsx @@ -129,7 +129,7 @@ export const FieldsDefinition: Partial> = { [EntityCoreFields.EModelExemplarMorphology]: { @@ -61,14 +66,12 @@ export const FieldsDefinition: Partial renderPreview( r as EntityCoreResource, { width: 184, height: 116 }, 'border border-neutral-3 h-full' ), - // renderImage(r as IEModel, { width: 196, height: 116 }, 'my-4'), vocabulary: { plural: 'responses', singular: 'response', @@ -143,4 +146,85 @@ export const FieldsDefinition: Partial { + return 'number_neurons' in r ? r.number_neurons : '-'; + }, + isDisplayable: true, + isFilterable: true, + defaultConstraint: { + lte: 'number_neurons__lte', + gte: 'number_neurons__gte', + }, + style: { width: 70 }, + }, + [EntityCoreFields.NumberSynapses]: { + title: 'Number of synapses', + filter: CoreFieldFilterTypeEnum.ValueRange, + render: (r) => { + return 'number_synapses' in r ? r.number_synapses : '-'; + }, + isDisplayable: true, + isFilterable: true, + defaultConstraint: { + lte: 'number_synapses__lte', + gte: 'number_synapses__gte', + }, + style: { width: 70 }, + }, + [EntityCoreFields.NumberConnections]: { + title: 'Number of connections', + filter: CoreFieldFilterTypeEnum.ValueRange, + render: (r) => { + return 'number_connections' in r ? r.number_connections : '-'; + }, + isDisplayable: true, + isFilterable: true, + defaultConstraint: { + lte: 'number_connections__lte', + gte: 'number_connections__gte', + }, + style: { width: 70 }, + }, + [EntityCoreFields.CircuitBuildCategory]: { + className: 'text-left', + title: 'Build category', + filter: CoreFieldFilterTypeEnum.DropdownList, + filterData: map(CircuitBuildCategory, (item) => ({ + label: item.label, + value: item.key, + })), + isFilterable: true, + isDisplayable: true, + render: (r) => + renderEmptyOrValue( + find(CircuitBuildCategory, { key: (r as ICircuit).build_category })?.label + ), + defaultConstraint: 'build_category__in', + vocabulary: { + plural: 'Build categories', + singular: 'Build category', + }, + style: { width: 184, align: 'left' }, + }, + [EntityCoreFields.CircuitScale]: { + className: 'text-left', + title: 'Scale', + filter: CoreFieldFilterTypeEnum.DropdownList, + filterData: map(CircuitScale, (item) => ({ + label: item.label, + value: item.key, + })), + defaultConstraint: 'scale__in', + isFilterable: true, + isDisplayable: true, + render: (r) => renderEmptyOrValue(find(CircuitScale, { key: (r as ICircuit).scale })?.label), + vocabulary: { + plural: 'Scales', + singular: 'Scale', + }, + style: { width: 184, align: 'left' }, + }, }; diff --git a/src/entity-configuration/definitions/types.ts b/src/entity-configuration/definitions/types.ts index 9cb335715..bdabb24f1 100644 --- a/src/entity-configuration/definitions/types.ts +++ b/src/entity-configuration/definitions/types.ts @@ -60,6 +60,11 @@ interface WithinListFilter extends Omit { value: Array; } +interface DropdownListFilter extends Omit { + type: CoreFieldFilterTypeEnum.DropdownList; + value: string | Array | null; +} + export type CoreFilter = | CheckListFilter | SearchFilter @@ -68,7 +73,8 @@ export type CoreFilter = | ValueFilter | ValueOrRangeFilter | BaseFilter - | WithinListFilter; + | WithinListFilter + | DropdownListFilter; type CoreFilterType = CoreFieldFilterTypeEnum | null; @@ -96,6 +102,7 @@ export type FieldDefinition = { title: string; description?: string; filter: CoreFilterType; + filterData?: any; defaultConstraint?: string | Record; perTypeConstraint?: Partial>; isSortable?: boolean; diff --git a/src/entity-configuration/domain/helpers.ts b/src/entity-configuration/domain/helpers.ts index 88fdb1624..aa7ea61ec 100644 --- a/src/entity-configuration/domain/helpers.ts +++ b/src/entity-configuration/domain/helpers.ts @@ -15,16 +15,42 @@ import { EntityCoreIdentifiable } from '@/api/entitycore/types/shared/global'; export type EntityCoreLegacyType = (typeof EntityCoreConfiguration)[keyof typeof EntityCoreConfiguration]['legacyType']; +/** + * Retrieves an entity configuration by its legacy type. + * + * @param params - An object containing the legacy type of the entity. + * @param params.legacyType - The legacy type of the entity (optional). + * @returns A promise resolving to the entity configuration(s) matching the given legacy type. + */ export const getEntityByLegacyType = ({ legacyType }: { legacyType?: EntityCoreLegacyType }) => find(EntityCoreConfiguration, { legacyType }); -// TODO: fix type to be a list of available types in entitycore +/** + * Retrieves an entity from the `EntityCoreConfiguration` based on the provided core entity type. + * + * @param params - An object containing the entity type to search for. + * @param params.type - The type of the entity to find (optional). + * @returns The entity matching the specified type, if found. + */ export const getEntityByCoreType = ({ type }: { type?: EntityTypeValue }) => find(EntityCoreConfiguration, { type }); +/** + * Retrieves an entity configuration by its slug value. + * + * @param param0 - An object containing the `slug` of the entity to retrieve. + * @returns The entity configuration matching the provided slug, or `undefined` if not found. + */ export const getEntityBySlug = ({ slug }: { slug: EntitySlugValue }) => find(EntityCoreConfiguration, { slug }); +/** + * Retrieves all entities from `EntityCoreConfiguration` that belong to the specified group. + * + * @param params - An object containing the group to filter entities by. + * @param params.group - The group of type `EntityCoreTypeGroup` to filter entities. + * @returns An array of entities from `EntityCoreConfiguration` that match the given group. + */ export const getEntitiesByGroup = ({ group }: { group: EntityCoreTypeGroup }) => { return filter(EntityCoreConfiguration, { group }); }; diff --git a/src/entity-configuration/domain/index.ts b/src/entity-configuration/domain/index.ts index 10df333a3..f5a30aca9 100644 --- a/src/entity-configuration/domain/index.ts +++ b/src/entity-configuration/domain/index.ts @@ -20,7 +20,6 @@ import { SimulationCampaign, } from '@/entity-configuration/domain/simulation'; -// NOTE: order is important (it's used in stats panel in explore) export const EntityCoreExperimentalConfiguration = { ReconstructionMorphology, ElectricalCellRecording, @@ -29,7 +28,6 @@ export const EntityCoreExperimentalConfiguration = { SynapsePerConnection, } as const; -// NOTE: order is important (it's used in stats panel in explore) export const EntityCoreModelConfiguration = { Emodel, MEmodel, diff --git a/src/entity-configuration/domain/model/circuit.ts b/src/entity-configuration/domain/model/circuit.ts index ea5c72399..14cef6407 100644 --- a/src/entity-configuration/domain/model/circuit.ts +++ b/src/entity-configuration/domain/model/circuit.ts @@ -3,8 +3,8 @@ import { DataType } from '@/constants/explore-section/list-views'; import { EntityTypeEnum } from '@/api/entitycore/types/entity-type'; import { EntitySlug } from '@/entity-configuration/domain/slug'; -import type { ICircuit } from '@/api/entitycore/types/entities/circuit'; import type { EntityCoreTypeConfig } from '@/entity-configuration/domain/types'; +import type { ICircuit } from '@/api/entitycore/types/entities/circuit'; export const Circuit: EntityCoreTypeConfig = { group: 'models', @@ -27,7 +27,6 @@ export const Circuit: EntityCoreTypeConfig = { }, asset: { extension: 'application/json', - // configfile: AssetLabel.single_neuron_synaptome_config, }, isBookmarkable: true, } as const; diff --git a/src/entity-configuration/domain/model/index.ts b/src/entity-configuration/domain/model/index.ts index a14e0db9c..8b877f124 100644 --- a/src/entity-configuration/domain/model/index.ts +++ b/src/entity-configuration/domain/model/index.ts @@ -1,8 +1,6 @@ // TODO: this data type should be moved from this file import { DataType } from '@/constants/explore-section/list-views'; -export * from '@/entity-configuration/domain/model/circuit'; - export const MODEL_DATATYPES = [ DataType.CircuitEModel, DataType.CircuitMEModel, diff --git a/src/features/listing-filter-panel/filter-as-dropdown.tsx b/src/features/listing-filter-panel/filter-as-dropdown.tsx new file mode 100644 index 000000000..88dbf41b6 --- /dev/null +++ b/src/features/listing-filter-panel/filter-as-dropdown.tsx @@ -0,0 +1,104 @@ +import { useCallback, useEffect, useState } from 'react'; +import { DefaultOptionType } from 'antd/es/select'; +import { Select } from 'antd'; +import map from 'lodash/map'; + +import { classNames } from '@/util/utils'; + +import type { CoreFilter } from '@/entity-configuration/definitions/types'; + +export type OptionType = DefaultOptionType; +type Props = { + filter: CoreFilter; + data?: Array; + onChange: (values: string[]) => void; + placeholder?: string; + allowMultiple?: boolean; +}; + +export default function DropdownList({ + filter, + data = [], + onChange, + placeholder = 'Select options...', + allowMultiple = false, +}: Props) { + const [selectedValues, setSelectedValues] = useState>([]); + + useEffect(() => { + if (filter.value) { + if (Array.isArray(filter.value)) { + setSelectedValues(filter.value as string[]); + } else if (typeof filter.value === 'string') { + setSelectedValues([filter.value]); + } + } else { + setSelectedValues([]); + } + }, [filter.value]); + + const handleChange = useCallback( + (value: string | string[]) => { + const newValues = Array.isArray(value) ? value : [value]; + setSelectedValues(newValues); + onChange(newValues); + }, + [onChange] + ); + + const handleClear = useCallback(() => { + setSelectedValues([]); + onChange([]); + }, [onChange]); + + const options = map(data, (item) => ({ + value: item.value, + label: item.count ? `${item.label} (${item.count})` : item.label, + key: item.id, + })); + + let value: string | string[] | undefined; + if (selectedValues.length > 0) { + if (allowMultiple) { + value = selectedValues; + } else { + [value] = selectedValues; + } + } else { + value = undefined; + } + + return ( +
+