diff --git a/.circleci/config.yml b/.circleci/config.yml index 3493dc88352..43064d62e72 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -298,9 +298,6 @@ jobs: - restore_cache: keys: - v10-cbio-database-files-{{ checksum "/tmp/db_data_md5key" }} - - restore_cache: - keys: - - v14-keycloak-database-files-{{ checksum "e2e-localdb-workspace/keycloak/keycloak-config-generated.json" }} - run: name: Init database command: | @@ -326,10 +323,6 @@ jobs: sudo chmod -R 777 $KC_DB_DATA_DIR && \ sudo chown -R circleci:circleci $KC_DB_DATA_DIR; \ fi - - save_cache: - paths: - - /tmp/repo/e2e-localdb-workspace/kc_db_data - key: v14-keycloak-database-files-{{ checksum "e2e-localdb-workspace/keycloak/keycloak-config-generated.json" }} - run: name: Run end-2-end tests with studies in local database command: | diff --git a/src/shared/components/query/CancerStudySelector.tsx b/src/shared/components/query/CancerStudySelector.tsx index d1d1417e8a7..0ff76b86e9f 100644 --- a/src/shared/components/query/CancerStudySelector.tsx +++ b/src/shared/components/query/CancerStudySelector.tsx @@ -57,6 +57,9 @@ export default class CancerStudySelector extends React.Component< onCheckAllFiltered: () => { this.logic.mainView.toggleAllFiltered(); }, + onCheckAuthorizedFiltered: () => { + this.logic.mainView.toggleAllAuthorizedAndFiltered(); + }, onClearFilter: () => { this.store.setSearchText(''); }, @@ -193,9 +196,16 @@ export default class CancerStudySelector extends React.Component< shownAndSelectedStudies, } = this.logic.mainView.getSelectionReport(); + const shownAndAuthorizedStudies = shownStudies.filter(study => { + return study.readPermission; + }); const quickSetButtons = this.logic.mainView.quickSelectButtons( getServerConfig().skin_quick_select_buttons ); + const shownStudiesLengthstring = + shownStudies.length < this.store.cancerStudies.result.length + ? 'matching filter' + : ''; return ( - + + + + + + + + diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 8a148cf794c..7beb6297a89 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -129,7 +129,7 @@ export class QueryStore { @computed get queryParser() { - return new QueryParser(this.referenceGenomes); + return new QueryParser(this.referenceGenomes, this.readPermissions); } initialize(urlWithInitialParams?: string) { @@ -1479,6 +1479,16 @@ export class QueryStore { return new Set(referenceGenomes); } + @computed get readPermissions(): Set { + const studies = Array.from(this.treeData.map_node_meta.keys()).filter( + s => typeof (s as CancerStudy).readPermission !== 'undefined' + ); + const readPermissions = studies.map(n => + (n as CancerStudy).readPermission.toString() + ); + return new Set(readPermissions); + } + @computed get selectableSelectedStudies() { return this.selectableSelectedStudyIds .map( diff --git a/src/shared/components/query/StudyListLogic.ts b/src/shared/components/query/StudyListLogic.ts index 53b9b27f0e8..549f0b1359a 100644 --- a/src/shared/components/query/StudyListLogic.ts +++ b/src/shared/components/query/StudyListLogic.ts @@ -400,6 +400,42 @@ export class FilteredCancerTreeView { ); } + @action toggleAllAuthorizedAndFiltered() { + const { + selectableSelectedStudyIds, + selectableSelectedStudies, + shownStudies, + shownAndSelectedStudies, + } = this.getSelectionReport(); + + let updatedSelectableSelectedStudyIds: string[] = []; + const shownAndAuthorizedStudies = shownStudies.filter(study => { + return study.readPermission; + }); + if ( + shownAndAuthorizedStudies.length === shownAndSelectedStudies.length + ) { + // deselect + updatedSelectableSelectedStudyIds = _.without( + this.store.selectableSelectedStudyIds, + ...shownAndAuthorizedStudies.map( + (study: CancerStudy) => study.studyId + ) + ); + } else { + updatedSelectableSelectedStudyIds = _.union( + this.store.selectableSelectedStudyIds, + shownAndAuthorizedStudies.map( + (study: CancerStudy) => study.studyId + ) + ); + } + + this.store.selectableSelectedStudyIds = updatedSelectableSelectedStudyIds.filter( + id => !_.includes(this.store.deletedVirtualStudies, id) + ); + } + @action selectAllMatchingStudies(match: string | string[]) { const { selectableSelectedStudyIds, diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index 81471833e85..7e98d668603 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,9 +129,12 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { + if (!_.has(study, fieldName)) { + continue; + } let anyPhraseMatch = false; const fieldValue = study[fieldName]; - if (fieldValue) { + if (typeof fieldValue !== 'undefined') { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || @@ -167,7 +170,8 @@ function matchPhrase(phrase: string, fullText: string) { /** * Full match using lowercase + * Need to convert boolean to string before applying lowercase */ -function matchPhraseFull(phrase: string, fullText: string) { - return fullText.toLowerCase() === phrase.toLowerCase(); +function matchPhraseFull(phrase: string, toMatch: boolean | string | number) { + return _.toString(toMatch).toLowerCase() === phrase.toLowerCase(); } diff --git a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts index 08267d28f94..f7c98bfa681 100644 --- a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts +++ b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts @@ -4,6 +4,10 @@ import { } from 'shared/components/query/filteredSearch/field/CheckboxFilterField'; import { CancerTreeSearchFilter } from 'shared/lib/query/textQueryUtils'; import { ListPhrase } from 'shared/components/query/filteredSearch/Phrase'; +import { + toFilterFieldOption, + toFilterFieldValue, +} from 'shared/components/query/filteredSearch/field/FilterFieldOption'; describe('CheckboxFilterField', () => { describe('createQueryUpdate', () => { @@ -12,7 +16,7 @@ describe('CheckboxFilterField', () => { nodeFields: ['studyId'], form: { input: FilterCheckbox, - options: ['a', 'b', 'c', 'd', 'e'], + options: ['a', 'b', 'c', 'd', 'e'].map(toFilterFieldOption), label: 'Test label', }, } as CancerTreeSearchFilter; @@ -48,7 +52,11 @@ describe('CheckboxFilterField', () => { it('removes all update when only And', () => { const checked = dummyFilter.form.options; const toRemove: ListPhrase[] = []; - const result = createQueryUpdate(toRemove, checked, dummyFilter); + const result = createQueryUpdate( + toRemove, + checked.map(toFilterFieldValue), + dummyFilter + ); expect(result.toAdd?.length).toEqual(0); }); diff --git a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx index facc250cc6a..e16ba935be0 100644 --- a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx +++ b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx @@ -13,11 +13,15 @@ import { } from 'shared/lib/query/textQueryUtils'; import { FieldProps } from 'shared/components/query/filteredSearch/field/FilterFormField'; import { ListPhrase } from 'shared/components/query/filteredSearch/Phrase'; +import { + FilterFieldOption, + toFilterFieldValue, +} from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export type CheckboxFilterField = { input: typeof FilterCheckbox; label: string; - options: string[]; + options: FilterFieldOption[]; }; export const FilterCheckbox: FunctionComponent = props => { @@ -43,9 +47,9 @@ export const FilterCheckbox: FunctionComponent = props => { }); for (const option of options) { - const isChecked = isOptionChecked(option, relevantClauses); + const isChecked = isOptionChecked(option.value, relevantClauses); if (isChecked) { - checkedOptions.push(option); + checkedOptions.push(option.value); } } @@ -53,9 +57,9 @@ export const FilterCheckbox: FunctionComponent = props => {
{props.filter.form.label}
- {options.map((option: string) => { - const id = `input-${option}`; - let isChecked = checkedOptions.includes(option); + {options.map((option: FilterFieldOption) => { + const id = `input-${option.displayValue}-${option.value}`; + let isChecked = checkedOptions.includes(option.value); return (
= props => { padding: '0 1em 0 0', }} > - { - isChecked = !isChecked; - updatePhrases(option, isChecked); - const update = createQueryUpdate( - toRemove, - checkedOptions, - props.filter - ); - props.onChange(update); - }} - style={{ - display: 'inline-block', - }} - />
); @@ -159,7 +163,8 @@ export function createQueryUpdate( toAdd = []; } else if (onlyNot || moreAnd) { const phrase = options - .filter(o => !optionsToAdd.includes(o)) + .filter(o => !optionsToAdd.includes(o.value)) + .map(toFilterFieldValue) .join(FILTER_VALUE_SEPARATOR); toAdd = [new NotSearchClause(createListPhrase(prefix, phrase, fields))]; } else { diff --git a/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts b/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts new file mode 100644 index 00000000000..facc45c616b --- /dev/null +++ b/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts @@ -0,0 +1,12 @@ +export type FilterFieldOption = { + value: string; + displayValue: string; +}; + +export function toFilterFieldOption(option: string) { + return { value: option, displayValue: option }; +} + +export function toFilterFieldValue(option: FilterFieldOption) { + return option.value; +} diff --git a/src/shared/components/query/filteredSearch/field/ListFormField.tsx b/src/shared/components/query/filteredSearch/field/ListFormField.tsx index e3bba5be7f1..8bfcde3e238 100644 --- a/src/shared/components/query/filteredSearch/field/ListFormField.tsx +++ b/src/shared/components/query/filteredSearch/field/ListFormField.tsx @@ -5,22 +5,22 @@ import { SearchClause } from 'shared/components/query/filteredSearch/SearchClaus import { Phrase } from 'shared/components/query/filteredSearch/Phrase'; import './ListFormField.scss'; import { toQueryString } from 'shared/lib/query/textQueryUtils'; +import { FilterFieldOption } from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export type ListFilterField = { label: string; input: typeof FilterList; - options: string[]; + options: FilterFieldOption[]; }; export const FilterList: FunctionComponent = props => { const form = props.filter.form as ListFilterField; const allPhrases = toUniquePhrases(props.query); - const queryString = toQueryString(props.query); return (
{props.filter.form.label}
{form.options.map(option => { - const update = props.parser.parseSearchQuery(option); + const update = props.parser.parseSearchQuery(option.value); return (
  • = props => { }); }} > - {option} + {option.displayValue}
  • ); diff --git a/src/shared/lib/query/QueryParser.spec.ts b/src/shared/lib/query/QueryParser.spec.ts index 4c655a9d60e..2e84ec80562 100644 --- a/src/shared/lib/query/QueryParser.spec.ts +++ b/src/shared/lib/query/QueryParser.spec.ts @@ -11,7 +11,7 @@ import { QueryParser } from 'shared/lib/query/QueryParser'; import { StringPhrase } from 'shared/components/query/filteredSearch/Phrase'; describe('QueryParser', () => { - const parser = new QueryParser(new Set()); + const parser = new QueryParser(new Set(), new Set()); const referenceGenomeFields = parser.searchFilters.find( f => f.phrasePrefix === 'reference-genome' )!.nodeFields; diff --git a/src/shared/lib/query/QueryParser.ts b/src/shared/lib/query/QueryParser.ts index 965ab857591..d9a317522be 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -6,9 +6,9 @@ import { import { AndSearchClause, FILTER_SEPARATOR, - SearchClause, NOT_PREFIX, NotSearchClause, + SearchClause, } from 'shared/components/query/filteredSearch/SearchClause'; import { FilterCheckbox } from 'shared/components/query/filteredSearch/field/CheckboxFilterField'; import { getServerConfig, ServerConfigHelpers } from 'config/config'; @@ -18,6 +18,7 @@ import { ListPhrase, Phrase, } from 'shared/components/query/filteredSearch/Phrase'; +import { toFilterFieldOption } from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export class QueryParser { /** @@ -25,7 +26,7 @@ export class QueryParser { */ private readonly _searchFilters: CancerTreeSearchFilter[]; - constructor(referenceGenomes: Set) { + constructor(referenceGenomes: Set, readPermissions: Set) { this._searchFilters = [ /** * Example queries: @@ -38,7 +39,7 @@ export class QueryParser { input: FilterList, options: ServerConfigHelpers.skin_example_study_queries( getServerConfig()!.skin_example_study_queries || '' - ), + ).map(toFilterFieldOption), }, }, /** @@ -49,10 +50,31 @@ export class QueryParser { nodeFields: ['referenceGenome'], form: { input: FilterCheckbox, - options: [...referenceGenomes], + options: [...referenceGenomes].map(toFilterFieldOption), label: 'Reference genome', }, }, + /** + * Show Authorized Studies + */ + { + phrasePrefix: 'authorized', + nodeFields: ['readPermission'], + form: { + input: FilterCheckbox, + options: + readPermissions.size > 1 + ? [ + { value: 'true', displayValue: 'Authorized' }, + { + value: 'false', + displayValue: 'Unauthorized', + }, + ] + : [], + label: 'Controlled access', + }, + }, ]; } diff --git a/src/shared/lib/query/textQueryUtils.spec.ts b/src/shared/lib/query/textQueryUtils.spec.ts index b083f5546a5..4ef8b9c150f 100644 --- a/src/shared/lib/query/textQueryUtils.spec.ts +++ b/src/shared/lib/query/textQueryUtils.spec.ts @@ -15,7 +15,7 @@ import { QueryParser } from 'shared/lib/query/QueryParser'; import { StringPhrase } from 'shared/components/query/filteredSearch/Phrase'; describe('textQueryUtils', () => { - const parser = new QueryParser(new Set()); + const parser = new QueryParser(new Set(), new Set()); const referenceGenomeFields = parser.searchFilters.find( f => f.phrasePrefix === 'reference-genome' )!.nodeFields;