-
Notifications
You must be signed in to change notification settings - Fork 175
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[dataquery] New Data Query Module (#8907)
This implements a completely new, CouchDB free data query module based on the LORIS data query framework and the API introduced in PR#8268. In the new module, all queries that are run by the user are saved into a history. The user can star or name a query in order to make it easier to find, rather than having to decide ahead of time that they want to save or share a query. Admins can pin queries (either to the top of the module or to the LORIS dashboard). The UI is also (generally) simplified and more context-sensitive. The "define filters" tab should be easier to understand. The view data tab has a new view where candidates are rows, fields are columns, and session-scoped variables are displayed inline in the cell in a table. Permissions are enforced for modules, candidates, and sessions.
- Loading branch information
Showing
45 changed files
with
7,014 additions
and
8,212 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
INSERT INTO permissions (code, description, moduleID) | ||
VALUES ( | ||
'dataquery_admin', | ||
'Dataquery Admin', | ||
(SELECT ID FROM modules WHERE Name='dataquery') | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import {ReactNode} from 'react'; | ||
|
||
type TableRow = (string|null)[] | ||
|
||
type Field = { | ||
show: boolean | ||
label: string | ||
} | ||
|
||
type hideOptions = { | ||
rowsPerPage: boolean | ||
downloadCSV: boolean | ||
defaultColumn: boolean | ||
} | ||
type DataTableProps = { | ||
data: TableRow[] | ||
rowNumLabel?: string | ||
getFormattedCell: (label: string, | ||
data: string, | ||
row: TableRow, | ||
headers: string[], | ||
fieldNo: number) => ReactNode | ||
onSort?: () => void | ||
hide?: hideOptions | ||
fields: Field[] | ||
nullTableShow?: boolean | ||
noDynamicTable?: boolean | ||
getMappedCell?: ( | ||
label: string, | ||
data: string|null, | ||
row: TableRow, | ||
headers: string[], | ||
fieldNo: number) => string|(string|null)[]|null | ||
} | ||
|
||
/** | ||
* The DataTable class. See DataTable.js | ||
*/ | ||
class DataTable { | ||
props: DataTableProps | ||
state: any | ||
context: object | ||
refs: {[key: string]: ReactInstance} | ||
|
||
/** | ||
* Construct a new modal | ||
*/ | ||
constructor(props: DataTableProps) | ||
|
||
/** | ||
* React lifecycle method | ||
* | ||
* @returns {ReactNode} | ||
*/ | ||
render(): ReactNode | ||
|
||
/** | ||
* React lifecycle method | ||
* | ||
* @param {object} newstate - the state to overwrite | ||
*/ | ||
setState(newstate: object): void | ||
|
||
/** | ||
* React lifecycle method | ||
*/ | ||
forceUpdate(): void | ||
} | ||
|
||
export default DataTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import {ReactNode} from 'react'; | ||
|
||
type PanelProps = { | ||
initCollapsed?: boolean | ||
collapsed?: boolean | ||
parentId?: string | ||
id?: string | ||
height?: string | ||
title?: string | ||
class?: string | ||
children: ReactNode | ||
views?: object | ||
collapsing?: boolean | ||
bold?: boolean | ||
panelSize?: string | ||
style?: React.CSSProperties | ||
} | ||
|
||
/** | ||
* The Modal class. See Modal.js | ||
*/ | ||
class Panel { | ||
props: PanelProps | ||
state: any | ||
context: object | ||
refs: {[key: string]: ReactInstance} | ||
|
||
/** | ||
* Construct a new modal | ||
*/ | ||
constructor(props: PanelProps) | ||
|
||
/** | ||
* React lifecycle method | ||
* | ||
* @returns {ReactNode} | ||
*/ | ||
render(): ReactNode | ||
|
||
/** | ||
* React lifecycle method | ||
* | ||
* @param {object} newstate - the state to overwrite | ||
*/ | ||
setState(newstate: object): void | ||
|
||
/** | ||
* React lifecycle method | ||
*/ | ||
forceUpdate(): void | ||
} | ||
|
||
export default Panel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {QueryTerm, QueryGroup} from './querydef'; | ||
import { | ||
APIQueryObject, | ||
APIQueryField, | ||
APIQueryGroupField, | ||
APIQueryCriteriaGroup, | ||
} from './types'; | ||
/** | ||
* Calculates the payload to submit to the search endpoint | ||
* to run the query. | ||
* | ||
* @param {APIQueryField[]} fields - the fields to query | ||
* @param {QueryGroup} filters - the root of the filters | ||
* @returns {APIQueryObject} - The query to send to the API | ||
*/ | ||
export function calcPayload( | ||
fields: APIQueryField[], | ||
filters: QueryGroup | ||
): APIQueryObject { | ||
const payload: APIQueryObject = { | ||
type: 'candidates', | ||
fields: fields.map((val: APIQueryField) => { | ||
const fieldpayload: APIQueryField = { | ||
module: val.module, | ||
category: val.category, | ||
field: val.field, | ||
}; | ||
if (val.visits) { | ||
fieldpayload.visits = val.visits; | ||
} | ||
return fieldpayload; | ||
}, | ||
), | ||
}; | ||
if (filters.group.length > 0) { | ||
payload.criteria = { | ||
operator: filters.operator, | ||
group: filters.group.map( (val) => { | ||
if (val instanceof QueryTerm) { | ||
return val as APIQueryGroupField; | ||
} else if (val instanceof QueryGroup) { | ||
return val as APIQueryCriteriaGroup; | ||
} else { | ||
throw new Error('Invalid query'); | ||
} | ||
}), | ||
}; | ||
} | ||
return payload; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
import Panel from 'jsx/Panel'; | ||
|
||
/** | ||
* Render a series of expansion panels | ||
* | ||
* @param {object} props - React props | ||
* @param {boolean?} props.alwaysOpen - If true, panels can not be toggled | ||
* @param {object} props.panels - Array of individual panels | ||
* @returns {React.ReactElement} - The panels | ||
*/ | ||
const ExpansionPanels = (props: { | ||
alwaysOpen?: boolean, | ||
panels: { | ||
title: string, | ||
content: React.ReactElement, | ||
defaultOpen?: boolean, | ||
alwaysOpen: boolean, | ||
}[] | ||
}) => { | ||
return ( | ||
<div className={'container-fluid'} | ||
style={{margin: '0 auto', maxWidth: '900px'}}> | ||
{ props.panels.map((panel, index) => ( | ||
<Panel | ||
key={index} | ||
title={panel.title} | ||
collapsed={panel.alwaysOpen} | ||
initCollapsed={panel.defaultOpen || props.alwaysOpen || true}> | ||
{panel.content} | ||
</Panel> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default ExpansionPanels; |
90 changes: 90 additions & 0 deletions
90
modules/dataquery/jsx/components/filterableselectgroup.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import Select, {SingleValue} from 'react-select'; | ||
|
||
type SelectOption = { | ||
label: string, | ||
value: string, | ||
module: string, | ||
}; | ||
|
||
type SelectGroup = { | ||
label: string, | ||
options: SelectOption[], | ||
}; | ||
/** | ||
* Render a select with option groups that can be | ||
* filtered | ||
* | ||
* @param {object} props - react props | ||
* @param {function} props.onChange - Callback on selection change | ||
* @param {string?} props.placeholder - An optional placeholder value when no elements are selected | ||
* @param {object} props.groups - Groups to select the dropdown into | ||
* @param {function} props.mapGroupName - A mapper from backend to frontend name for groups | ||
* @returns {React.ReactElement} - The element | ||
*/ | ||
function FilterableSelectGroup(props: { | ||
onChange: (module: string, value: string) => void, | ||
placeholder?: string, | ||
groups: object, | ||
mapGroupName?: (module: string) => string, | ||
}) { | ||
const groups: SelectGroup[] = []; | ||
const placeholder = props.placeholder || 'Select a category'; | ||
for (const [module, subcategories] | ||
of Object.entries(props.groups)) { | ||
const options: SelectOption[] = []; | ||
for (const [value, desc] | ||
of Object.entries(subcategories) as unknown as [string, string]) { | ||
options.push({ | ||
value: value, | ||
label: desc, | ||
module: module, | ||
}); | ||
} | ||
|
||
let label = module; | ||
if (props.mapGroupName) { | ||
label = props.mapGroupName(module); | ||
} | ||
groups.push({ | ||
label: label, | ||
options: options, | ||
}); | ||
} | ||
|
||
/** | ||
* Callback to call when the selection changes. | ||
* | ||
* @param {object} e - The click event callback | ||
* @param {string} e.module - The module | ||
* @param {string} e.value - The value | ||
* @returns {void} | ||
*/ | ||
const selected = (e: SingleValue<SelectGroup>) => { | ||
// The callback should be (e: SelectOption) but typescript | ||
// is convinced that it's a SingleValue<SelectGroup>. | ||
// console.log(e) confirms that it has the same structure | ||
// as SelectOption, so just convert it and explicitly | ||
// cast it unsafely to make the compiler happy. | ||
const val: SelectOption = e as unknown as SelectOption; | ||
props.onChange(val.module, val.value); | ||
}; | ||
return ( | ||
<div> | ||
<Select options={groups} onChange={selected} | ||
menuPortalTarget={document.body} | ||
styles={{menuPortal: | ||
/** | ||
* Add a z-index to ensure the element stays visible | ||
* | ||
* @param {object} base - the base CSS | ||
* @returns {object} - the new CSS with z-index added | ||
*/ | ||
(base) => ({...base, zIndex: 9999})} | ||
} | ||
placeholder={placeholder} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
export default FilterableSelectGroup; |
Oops, something went wrong.