From ca4fee5c09a998523e56eead2e0071e5c9a37f6c Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:07:20 -0700 Subject: [PATCH 1/7] Add 'show experiments' URL param --- .../datasetSelectionUntrrackedStore.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts index 58ee30c2..d6779791 100644 --- a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts +++ b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts @@ -1,6 +1,7 @@ import { ref, computed, watch } from 'vue'; import { defineStore } from 'pinia'; import { asyncComputed } from '@vueuse/core'; +import { useRoute } from 'vue-router'; import * as vg from '@uwdata/vgplot'; import { computedAsync } from '@vueuse/core'; @@ -70,6 +71,8 @@ export const useDatasetSelectionStore = defineStore( const experimentDataInitialized = computed(() => { return experimentDataLoaded.value; }); + + const route = useRoute(); let controller: AbortController; const compTableName = computed(() => { @@ -96,6 +99,10 @@ export const useDatasetSelectionStore = defineStore( // Generate Experiment List const experimentFilenameList = asyncComputed(async () => { + // Access dependencies synchronously to ensure tracking + const query = route.query; + const currentFilename = datasetSelectionTrrackedStore.currentExperimentFilename; + if (configStore.serverUrl == null) return null; const fullURL = configStore.getFileUrl( configStore.entryPointFilename @@ -136,7 +143,20 @@ export const useDatasetSelectionStore = defineStore( vg.coordinator().databaseConnector(connector); const data = await response.json(); - return data.experiments; + + // --- Show all experiments if show-experiments is in query --- + const allExperiments = data.experiments; + + if ( + 'show-experiments' in query || + currentFilename == null + ) { + return allExperiments; + } else { + return [ + currentFilename, + ]; + } }, [refreshTime.value]); // Sets location Metadata From bbd49099a13591a8a496ce6d6608df5576aa0499 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:04:46 -0700 Subject: [PATCH 2/7] Add --show-experiments build flag --- .../docker-compose-local.template.yml | 5 +---- .../datasetSelectionUntrrackedStore.ts | 17 +++++------------ build.py | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.build-files/docker-compose-local.template.yml b/.build-files/docker-compose-local.template.yml index f651e5f5..d46d3013 100644 --- a/.build-files/docker-compose-local.template.yml +++ b/.build-files/docker-compose-local.template.yml @@ -15,9 +15,6 @@ services: NGINX_FILE: ${NGINX_FILE} ports: - "${LOCAL_PORT_1:-80}:${LOCAL_PORT_2:-80}" - depends_on: - - server - - celery env_file: - path: ${DOCKER_ENV_FILE} # relative to docker compose (has to be in same directory) required: true @@ -41,4 +38,4 @@ services: volumes: duckdb-data: - migrations_volume: \ No newline at end of file + migrations_volume: diff --git a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts index d6779791..825d286f 100644 --- a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts +++ b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts @@ -1,7 +1,7 @@ import { ref, computed, watch } from 'vue'; import { defineStore } from 'pinia'; import { asyncComputed } from '@vueuse/core'; -import { useRoute } from 'vue-router'; + import * as vg from '@uwdata/vgplot'; import { computedAsync } from '@vueuse/core'; @@ -72,7 +72,6 @@ export const useDatasetSelectionStore = defineStore( return experimentDataLoaded.value; }); - const route = useRoute(); let controller: AbortController; const compTableName = computed(() => { @@ -100,7 +99,6 @@ export const useDatasetSelectionStore = defineStore( // Generate Experiment List const experimentFilenameList = asyncComputed(async () => { // Access dependencies synchronously to ensure tracking - const query = route.query; const currentFilename = datasetSelectionTrrackedStore.currentExperimentFilename; if (configStore.serverUrl == null) return null; @@ -144,18 +142,13 @@ export const useDatasetSelectionStore = defineStore( const data = await response.json(); - // --- Show all experiments if show-experiments is in query --- const allExperiments = data.experiments; + const showExperiments = import.meta.env.VITE_SHOW_EXPERIMENTS === 'true'; - if ( - 'show-experiments' in query || - currentFilename == null - ) { + if (showExperiments || currentFilename == null) { return allExperiments; } else { - return [ - currentFilename, - ]; + return [currentFilename]; } }, [refreshTime.value]); @@ -304,7 +297,7 @@ export const useDatasetSelectionStore = defineStore( .locationMetadataList) { if ( datasetSelectionTrrackedStore.selectedLocationIds[ - location.id + location.id ] ) { return location; diff --git a/build.py b/build.py index dc5b9ac0..583bbc0a 100644 --- a/build.py +++ b/build.py @@ -81,7 +81,7 @@ def createComposeFile(local=False, nfs=False): shutil.copy(docker_compose_template, '.build-files/docker-compose.yml') -def createEnvFile(configFileName, envFileName, useDid=False): +def createEnvFile(configFileName, envFileName, useDid=False, showExperiments=False): buildConfig = BuildConfig.BuildConfig(configFileName, envFileName) buildConfig.reportErrors() @@ -110,6 +110,7 @@ def createEnvFile(configFileName, envFileName, useDid=False): buildConfig.set('VITE_ENVIRONMENT', environment) buildConfig.set('VITE_SERVER_URL', f'{base_url}/data') + buildConfig.set('VITE_SHOW_EXPERIMENTS', str(showExperiments).lower()) localPort1 = buildConfig.get("generalSettings.local_port_1") buildConfig.set("LOCAL_PORT_1", localPort1) @@ -390,14 +391,16 @@ def cleanup_and_exit(signal=None, frame=None): sys.exit(0) -def prepare_dev(buildConfig): +def prepare_dev(buildConfig, showExperiments=False): vite_server_url = f"{buildConfig.get('generalSettings.baseUrl')}/data" vite_use_http = f"{buildConfig.get('generalSettings.useHttp')}" vite_environment = f"{buildConfig.get('generalSettings.environment')}" + vite_show_experiments = str(showExperiments).lower() outString = f"VITE_SERVER_URL={vite_server_url}\n" \ f"VITE_USE_HTTP={vite_use_http}\n" \ - f"VITE_ENVIRONMENT={vite_environment}\n" + f"VITE_ENVIRONMENT={vite_environment}\n" \ + f"VITE_SHOW_EXPERIMENTS={vite_show_experiments}\n" fullEnvFileName = 'apps/client/.env' with open(fullEnvFileName, 'w') as outF: @@ -445,6 +448,8 @@ def spinner(msg): help="Generates .env file for client environment.") parser.add_argument("-i", "--use-did", action="store_true", required=False, help="Flag to specify that this build script is running in a DiD setup.") + parser.add_argument("--show-experiments", action="store_true", required=False, + help="Enable experiment switching in the UI.") args = parser.parse_args() @@ -464,7 +469,7 @@ def spinner(msg): print('No client .env file found.') pass - buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did) + buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.show_experiments) use_http = buildConfig.get('generalSettings.useHttp') if use_http: @@ -478,7 +483,7 @@ def spinner(msg): createComposeFile(local=buildConfig.local, nfs=buildConfig.nfs) if buildConfig.local: - services = ["db", "client", "server", "data", "celery", "redis", "duckdb"] + services = ["client", "data", "duckdb"] else: services = ["db", "client", "server", "minio", "celery", "redis", "duckdb"] @@ -513,7 +518,7 @@ def spinner(msg): follow_all_logs(logs_path, services, args.verbose, args.detached) if args.prepare_dev: - prepare_dev(buildConfig) + prepare_dev(buildConfig, args.show_experiments) check_containers_status(services, args.detached) else: @@ -522,5 +527,5 @@ def spinner(msg): if args.overwrite: config_file_name = overwrite_config(config_file_name) - buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did) + buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.show_experiments) createComposeFile(local=buildConfig.local, nfs=buildConfig.nfs) From 3e5e0fc2eb2d84e5572bcc6f3d3addf39f225136 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:29:11 -0700 Subject: [PATCH 3/7] ?show-all-experiments url param and build-flag and aa_index.json visibility description --- .../datasetSelectionUntrrackedStore.ts | 45 ++++++++++++------- build.py | 22 ++++----- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts index 825d286f..a3cbf797 100644 --- a/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts +++ b/apps/client/src/stores/dataStores/datasetSelectionUntrrackedStore.ts @@ -1,7 +1,7 @@ import { ref, computed, watch } from 'vue'; import { defineStore } from 'pinia'; import { asyncComputed } from '@vueuse/core'; - +import { useRoute } from 'vue-router'; import * as vg from '@uwdata/vgplot'; import { computedAsync } from '@vueuse/core'; @@ -45,7 +45,7 @@ export interface LocationMetadata { imageDataFilename?: string; segmentationsFolder?: string; tags?: Tags; - // name?: string; // user friendly name + name?: string; // user friendly name // condition?: string; // experimental condition // TODO: - does this need to be an array // plate?: string; // well?: string; @@ -71,7 +71,6 @@ export const useDatasetSelectionStore = defineStore( const experimentDataInitialized = computed(() => { return experimentDataLoaded.value; }); - let controller: AbortController; const compTableName = computed(() => { @@ -97,11 +96,9 @@ export const useDatasetSelectionStore = defineStore( }) // Generate Experiment List + const route = useRoute(); const experimentFilenameList = asyncComputed(async () => { - // Access dependencies synchronously to ensure tracking - const currentFilename = datasetSelectionTrrackedStore.currentExperimentFilename; - - if (configStore.serverUrl == null) return null; + if (configStore.serverUrl == null) return []; const fullURL = configStore.getFileUrl( configStore.entryPointFilename ); @@ -120,12 +117,12 @@ export const useDatasetSelectionStore = defineStore( `Could not access ${fullURL}. "${error.message}"` ); }); - if (response == null) return; + if (response == null) return []; if (!response.ok) { handleFetchEntryError( `Server Error. "${response.status}: ${response.statusText}"` ); - return; + return []; } fetchingEntryFile.value = false; serverUrlValid.value = true; @@ -141,15 +138,33 @@ export const useDatasetSelectionStore = defineStore( vg.coordinator().databaseConnector(connector); const data = await response.json(); + const rawExperiments: (string | { filename: string; 'visible-by-default'?: boolean })[] = data.experiments; - const allExperiments = data.experiments; - const showExperiments = import.meta.env.VITE_SHOW_EXPERIMENTS === 'true'; + // Normalize entries: extract filename from objects, pass strings through + const allFilenames = rawExperiments.map((entry) => + typeof entry === 'string' ? entry : entry.filename + ); - if (showExperiments || currentFilename == null) { - return allExperiments; - } else { - return [currentFilename]; + // If experiment visibility control is not enabled, show all experiments + const visibilityControlEnabled = + import.meta.env.VITE_EXPERIMENT_VISIBILITY_CONTROL === 'true'; + if (!visibilityControlEnabled) { + return allFilenames; } + + // If show-all-experiments URL param is present, show all + if ('show-all-experiments' in route.query) { + return allFilenames; + } + + // Filter to only visible-by-default experiments + return rawExperiments + .filter((entry) => + typeof entry === 'object' && entry['visible-by-default'] === true + ) + .map((entry) => + typeof entry === 'string' ? entry : entry.filename + ); }, [refreshTime.value]); // Sets location Metadata diff --git a/build.py b/build.py index 583bbc0a..6fafcf3e 100644 --- a/build.py +++ b/build.py @@ -81,7 +81,7 @@ def createComposeFile(local=False, nfs=False): shutil.copy(docker_compose_template, '.build-files/docker-compose.yml') -def createEnvFile(configFileName, envFileName, useDid=False, showExperiments=False): +def createEnvFile(configFileName, envFileName, useDid=False, experimentVisibilityControl=False): buildConfig = BuildConfig.BuildConfig(configFileName, envFileName) buildConfig.reportErrors() @@ -110,7 +110,7 @@ def createEnvFile(configFileName, envFileName, useDid=False, showExperiments=Fal buildConfig.set('VITE_ENVIRONMENT', environment) buildConfig.set('VITE_SERVER_URL', f'{base_url}/data') - buildConfig.set('VITE_SHOW_EXPERIMENTS', str(showExperiments).lower()) + buildConfig.set('VITE_EXPERIMENT_VISIBILITY_CONTROL', str(experimentVisibilityControl).lower()) localPort1 = buildConfig.get("generalSettings.local_port_1") buildConfig.set("LOCAL_PORT_1", localPort1) @@ -391,16 +391,16 @@ def cleanup_and_exit(signal=None, frame=None): sys.exit(0) -def prepare_dev(buildConfig, showExperiments=False): +def prepare_dev(buildConfig, experimentVisibilityControl=False): vite_server_url = f"{buildConfig.get('generalSettings.baseUrl')}/data" vite_use_http = f"{buildConfig.get('generalSettings.useHttp')}" vite_environment = f"{buildConfig.get('generalSettings.environment')}" - vite_show_experiments = str(showExperiments).lower() + vite_experiment_visibility_control = str(experimentVisibilityControl).lower() outString = f"VITE_SERVER_URL={vite_server_url}\n" \ f"VITE_USE_HTTP={vite_use_http}\n" \ f"VITE_ENVIRONMENT={vite_environment}\n" \ - f"VITE_SHOW_EXPERIMENTS={vite_show_experiments}\n" + f"VITE_EXPERIMENT_VISIBILITY_CONTROL={vite_experiment_visibility_control}\n" fullEnvFileName = 'apps/client/.env' with open(fullEnvFileName, 'w') as outF: @@ -448,8 +448,10 @@ def spinner(msg): help="Generates .env file for client environment.") parser.add_argument("-i", "--use-did", action="store_true", required=False, help="Flag to specify that this build script is running in a DiD setup.") - parser.add_argument("--show-experiments", action="store_true", required=False, - help="Enable experiment switching in the UI.") + parser.add_argument("--experiment-visibility-control", action="store_true", required=False, + help="Enable experiment visibility control. Only experiments with " + "'visible-by-default' in aa_index.json will be shown unless " + "'show-all-experiments' URL param is used.") args = parser.parse_args() @@ -469,7 +471,7 @@ def spinner(msg): print('No client .env file found.') pass - buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.show_experiments) + buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.experiment_visibility_control) use_http = buildConfig.get('generalSettings.useHttp') if use_http: @@ -518,7 +520,7 @@ def spinner(msg): follow_all_logs(logs_path, services, args.verbose, args.detached) if args.prepare_dev: - prepare_dev(buildConfig, args.show_experiments) + prepare_dev(buildConfig, args.experiment_visibility_control) check_containers_status(services, args.detached) else: @@ -527,5 +529,5 @@ def spinner(msg): if args.overwrite: config_file_name = overwrite_config(config_file_name) - buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.show_experiments) + buildConfig = createEnvFile(config_file_name, args.env_file, args.use_did, args.experiment_visibility_control) createComposeFile(local=buildConfig.local, nfs=buildConfig.nfs) From 15d9dcbc90c9783c908465c538412b5a400b06f6 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:11:03 -0700 Subject: [PATCH 4/7] Undo changes to local template --- .build-files/docker-compose-local.template.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.build-files/docker-compose-local.template.yml b/.build-files/docker-compose-local.template.yml index d46d3013..6bd6eecd 100644 --- a/.build-files/docker-compose-local.template.yml +++ b/.build-files/docker-compose-local.template.yml @@ -15,6 +15,9 @@ services: NGINX_FILE: ${NGINX_FILE} ports: - "${LOCAL_PORT_1:-80}:${LOCAL_PORT_2:-80}" + depends_on: + - server + - celery env_file: - path: ${DOCKER_ENV_FILE} # relative to docker compose (has to be in same directory) required: true @@ -38,4 +41,3 @@ services: volumes: duckdb-data: - migrations_volume: From e11e57185f19c2761699fe446ccfa66b02f35564 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:11:44 -0700 Subject: [PATCH 5/7] Undo last local change --- .build-files/docker-compose-local.template.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.build-files/docker-compose-local.template.yml b/.build-files/docker-compose-local.template.yml index 6bd6eecd..bd699b5f 100644 --- a/.build-files/docker-compose-local.template.yml +++ b/.build-files/docker-compose-local.template.yml @@ -41,3 +41,4 @@ services: volumes: duckdb-data: + migrations_volume: From fd813246ba512dcde19e29fedea4d9325bd97e7d Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:14:20 -0700 Subject: [PATCH 6/7] Undo local container deletion --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 6fafcf3e..9642ff11 100644 --- a/build.py +++ b/build.py @@ -485,7 +485,7 @@ def spinner(msg): createComposeFile(local=buildConfig.local, nfs=buildConfig.nfs) if buildConfig.local: - services = ["client", "data", "duckdb"] + services = ["db", "client", "server", "data", "celery", "redis", "duckdb"] else: services = ["db", "client", "server", "minio", "celery", "redis", "duckdb"] From d3c94d59d725c78c7524d60112c2cb978b3677d0 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:18:56 -0700 Subject: [PATCH 7/7] Add readme --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 170cda62..8cd187cc 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,30 @@ In the root of the repository, run the following following command: python3 build.py --prepare-dev ``` +### Prepare Development Environment and Run Separately with Experiment Visibility Control + +Experiment visibility control allows users to control which experiments are visible to them. You specify which experiments are visible to users in the `aa_index.json` file. + +```bash +python3 build.py --prepare-dev --experiment-visibility-control +``` + +Experiment visibility control example: + +```json +{ + "experiments": [ + { + "filename": "Data Discovery - Automated Cell Lineage Tracking.json", + "visible-by-default": true + }, + "Quality Control - Cancer Triggering Microenvironments.json", + "Communication - Tumorigenic Cell States.json" + ] +} +``` + + This will generate the necessary environment file in the client directory while still running the rest of the container. Then, the development server can be started separately by the following: ```bash @@ -109,6 +133,7 @@ The build script will do its best to validate the config before starting the doc | -o, --overwrite | When set, any settings in your configuration file which are present as environment variables in the current session will be overwritten. | -o | | -s, --disable-spinner | When set, disables inline spinner | -s | | --prepare-dev | When set, will create the `.env` file based on the current configuration that is required to run a separate client development server. | --prepare-dev | +| --experiment-visibility-control | When set, will create the `.env` file based on the current configuration that is required to run a separate client development server. | --experiment-visibility-control| In the repository, you will see two docker directories: `docker` and `docker-local`. The main deployment will use the `docker` directory. The `docker-local` directory is a separate local version of Loon which we will discuss shortly. Below are some examples.