Skip to content

Commit

Permalink
Merge branch 'develop' into feature/new-dashboard-load-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsdejong committed Oct 27, 2023
2 parents e2ff9bc + 3a2cf4d commit 1fbf33a
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 35 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/modules/ROOT/images/select-single-table.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docs/modules/ROOT/pages/user-guide/reports/table.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,22 @@ following style rules can be applied to the table:
- The text color of a single cell in the table.

If a column is hidden (header prefixed with __ double underscore), it can still be used as an entry point for a styling rule.

== Report Actions

With the link:../../extensions/report-actions[Report Actions] extension, tables can be turned into interactive components that set parameters.
Two flavours of report actions for tables exist:

=== 1. Select a value from a row
Adding a **Cell Click** action to a table column, turns the values in that row into clickable buttons.
When the user clicks on the button, a predefined parameter is set to one of the columns in that row.

image::select-single-table.png[Select a value from a table to be used as a parameter]

=== 2. Select multiple from a row
Adding a **Row Clicked** action to a table prepends each row with a checkbox.
The user can then check one or more boxes to update a dashboard parameter.

> Keep in mind that regardless if one or more values are selected, the type of the dashboard parameter is a list of values. The queries using the parameter must ensure that the list type is handled correctly.

image::select-multiple-table.png[Select multiple values to be used as a parameter]
4 changes: 4 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
border: none !important;
}

.MuiDataGrid-footerContainer > div {
margin-top: -40px;
}

.MuiChip-root:before {
border: none !important;
}
Expand Down
2 changes: 1 addition & 1 deletion src/chart/Chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface ChartProps {
parameters?: Record<string, any>; // A dictionary with the global dashboard parameters.
queryCallback?: (query: string | undefined, parameters: Record<string, any>, setRecords: any) => void; // Callback to query the database with a given set of parameters. Calls 'setReccords' upon completion.
createNotification?: (title: string, message: string) => void; // Callback to create a notification that overlays the entire application.
setGlobalParameter?: (name: string, value: string) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports.
setGlobalParameter?: (name: string, value: any) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports.
getGlobalParameter?: (name) => string; // Allows a chart to get a global dashboard parameter.
updateReportSetting?: (name, value) => void; // Callback to update a setting for this report.
fields: (fields) => string[]; // List of fields (return values) available for the report.
Expand Down
26 changes: 25 additions & 1 deletion src/chart/parameter/component/NodePropertyParameterSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { debounce, TextField } from '@mui/material';
import Autocomplete from '@mui/material/Autocomplete';
import { ParameterSelectProps } from './ParameterSelect';
Expand All @@ -24,6 +24,7 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {
const { multiSelector, manualParameterSave } = props;
const allParameters = props.allParameters ? props.allParameters : {};
const [extraRecords, setExtraRecords] = React.useState([]);

const [inputDisplayText, setInputDisplayText] = React.useState(
props.parameterDisplayValue && multiSelector ? '' : props.parameterDisplayValue
);
Expand All @@ -34,6 +35,7 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {

const debouncedQueryCallback = useCallback(debounce(props.queryCallback, suggestionsUpdateTimeout), []);
const label = props.settings && props.settings.entityType ? props.settings.entityType : '';
const multiSelectLimit = props.settings && props.settings.multiSelectLimit ? props.settings.multiSelectLimit : 5;
const propertyType = props.settings && props.settings.propertyType ? props.settings.propertyType : '';
const helperText = props.settings && props.settings.helperText ? props.settings.helperText : '';
const clearParameterOnFieldClear =
Expand Down Expand Up @@ -121,11 +123,33 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {
handleParametersUpdate(newValue, newDisplay, manualParameterSave);
};

useEffect(() => {
// Handle external updates of parameter values, with varying value types and parameter selector types.
// Handles multiple scenarios if an external parameter changes type from value to lists.
const isArray = Array.isArray(props.parameterDisplayValue);
if (multiSelector) {
if (isArray) {
setInputDisplayText(props.parameterDisplayValue);
setInputValue(props.parameterDisplayValue);
} else if (props.parameterDisplayValue !== '') {
setInputDisplayText([props.parameterDisplayValue]);
setInputValue([props.parameterDisplayValue]);
} else {
setInputDisplayText('');
setInputValue([]);
}
} else {
setInputDisplayText(props.parameterDisplayValue);
setInputValue(props.parameterDisplayValue);
}
}, [props.parameterDisplayValue]);

return (
<div className={'n-flex n-flex-row n-flex-wrap n-items-center'}>
<Autocomplete
id='autocomplete'
multiple={multiSelector}
limitTags={multiSelectLimit}
options={extraRecords.map((r) => r?._fields?.[displayValueRowIndex] || '(no data)').sort()}
style={{
maxWidth: 'calc(100% - 40px)',
Expand Down
46 changes: 46 additions & 0 deletions src/chart/table/TableActionsHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const hasCheckboxes = (actionsRules) => {
let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');
return rules.length > 0;
};

export const getCheckboxes = (actionsRules, rows, getGlobalParameter) => {
let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');
const params = rules.map((rule) => `neodash_${rule.customizationValue}`);
// See if any of the rows should be checked. This is the case when a parameter is already in the list of checked values.
let selection: number[] = [];
params.forEach((parameter, index) => {
const fieldName = rules[index].value;
const values = getGlobalParameter(parameter);

// If the parameter is an array (to be expected), iterate over it to find the rows to check.
if (Array.isArray(values)) {
values.forEach((value) => {
rows.forEach((row, index) => {
if (row[fieldName] == value) {
selection.push(index);
}
});
});
} else {
// Else (special case), still check the row if it's a single value parameter.
rows.forEach((row, index) => {
if (row[fieldName] == values) {
selection.push(index);
}
});
}
});
return [...new Set(selection)];
};

export const updateCheckBoxes = (actionsRules, rows, selection, setGlobalParameter) => {
if (hasCheckboxes(actionsRules)) {
const selectedRows = rows.filter((_, i) => selection.includes(i));
let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');
rules.forEach((rule) => {
const parameterValues = selectedRows.map((row) => row[rule.value]).filter((v) => v !== undefined);
setGlobalParameter(`neodash_${rule.customizationValue}`, parameterValues);
setGlobalParameter(`neodash_${rule.customizationValue}_display`, parameterValues);
});
}
};
6 changes: 6 additions & 0 deletions src/chart/table/TableChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CloudArrowDownIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/ic
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Button from '@mui/material/Button';
import { extensionEnabled } from '../../utils/ReportUtils';
import { getCheckboxes, hasCheckboxes, updateCheckBoxes } from './TableActionsHelper';

const TABLE_HEADER_HEIGHT = 32;
const TABLE_FOOTER_HEIGHT = 62;
Expand Down Expand Up @@ -268,6 +269,11 @@ export const NeoTableChart = (props: ChartProps) => {
navigator.clipboard.writeText(e.value);
}
}}
checkboxSelection={hasCheckboxes(actionsRules)}
selectionModel={getCheckboxes(actionsRules, rows, props.getGlobalParameter)}
onSelectionModelChange={(selection) =>
updateCheckBoxes(actionsRules, rows, selection, props.setGlobalParameter)
}
pageSize={tablePageSize > 0 ? tablePageSize : 5}
rowsPerPageOptions={rows.length < 5 ? [rows.length, 5] : [5]}
disableSelectionOnClick
Expand Down
2 changes: 2 additions & 0 deletions src/component/field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const NeoField = ({
select = false,
disabled = undefined,
variant = undefined,
password = false,
helperText = undefined,
defaultValueLabel = undefined,
defaultValue = undefined,
Expand Down Expand Up @@ -77,6 +78,7 @@ const NeoField = ({
variant={variant}
label={label}
helpText={helperText}
type={password ? 'password' : 'text'}
disabled={disabled}
value={value != null ? value : defaultValue}
fluid
Expand Down
2 changes: 2 additions & 0 deletions src/component/field/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const NeoSetting = ({
defaultValue,
disabled = undefined,
helperText = undefined,
password = false,
onChange,
onClick = () => {},
style = { width: '100%', marginBottom: '10px', marginRight: '10px', marginLeft: '10px' },
Expand Down Expand Up @@ -104,6 +105,7 @@ const NeoSetting = ({
disabled={disabled}
helperText={helperText}
value={value}
password={password}
defaultValue={''}
placeholder={`${defaultValue}`}
style={style}
Expand Down
5 changes: 5 additions & 0 deletions src/config/ReportConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,11 @@ const _REPORT_TYPES = {
values: [true, false],
default: false,
},
multiSelectLimit: {
label: 'Multiselect Value Limit',
type: SELECTION_TYPES.NUMBER,
default: 5,
},
helperText: {
label: 'Helper Text (Override)',
type: SELECTION_TYPES.TEXT,
Expand Down
85 changes: 52 additions & 33 deletions src/extensions/actions/ActionsRuleCreationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const RULE_CONDITIONS = {
value: 'doubleClick',
label: 'Cell Double Click',
},
{
value: 'rowCheck',
label: 'Row Checked',
disableFieldSelection: true,
multiple: true,
},
],
map: [
{
Expand Down Expand Up @@ -274,6 +280,7 @@ export const NeoCustomReportActionsModal = ({
(el) => el.value === rule.customization
);
const ruleTrigger = RULE_CONDITIONS[type].find((el) => el.value === rule.condition);

return (
<>
<tr>
Expand All @@ -286,7 +293,11 @@ export const NeoCustomReportActionsModal = ({
<Dropdown
type='select'
className='n-align-middle n-w-2/5 n-pr-1'
style={{ minWidth: 80, display: 'inline-block' }}
style={{
minWidth: '140px',
width: ruleTrigger.disableFieldSelection === true ? '100%' : '140px',
display: 'inline-block',
}}
selectProps={{
onChange: (newValue) => updateRuleField(index, 'condition', newValue.value),
options:
Expand All @@ -298,40 +309,46 @@ export const NeoCustomReportActionsModal = ({
value: { label: ruleTrigger ? ruleTrigger.label : '', value: rule.condition },
}}
></Dropdown>
<Autocomplete
className='n-align-middle n-inline-block n-w-3/5'
disableClearable={true}
id='autocomplete-label-type'
size='small'
noOptionsText='*Specify an exact field name'
options={createFieldVariableSuggestionsFromRule(rule, true)}
value={rule.field ? rule.field : ''}
inputValue={rule.field ? rule.field : ''}
popupIcon={<></>}
style={{
minWidth: 125,
}}
onInputChange={(event, value) => {
updateRuleField(index, 'field', value);
}}
onChange={(event, newValue) => {
updateRuleField(index, 'field', newValue);
}}
renderInput={(params) => (
<TextField
{...params}
placeholder='Field name...'
style={{ padding: 0 }}
InputLabelProps={{ shrink: true }}
/>
)}
/>
{!ruleTrigger.disableFieldSelection ? (
<Autocomplete
className='n-align-middle n-inline-block n-w-3/5'
disableClearable={true}
id='autocomplete-label-type'
size='small'
noOptionsText='*Specify an exact field name'
options={createFieldVariableSuggestionsFromRule(rule, true)}
value={rule.field ? rule.field : ''}
inputValue={rule.field ? rule.field : ''}
popupIcon={<></>}
style={{
minWidth: 125,
}}
onInputChange={(event, value) => {
updateRuleField(index, 'field', value);
}}
onChange={(event, newValue) => {
updateRuleField(index, 'field', newValue);
}}
renderInput={(params) => (
<TextField
{...params}
placeholder='Field name...'
style={{ padding: 0 }}
InputLabelProps={{ shrink: true }}
/>
)}
/>
) : (
<></>
)}
</div>
</td>
<td width='5%' className='n-text-center'>
<span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>SET</span>
<td width='6%' className='n-text-center'>
<span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>
{!ruleTrigger.multiple ? 'SET' : 'APPEND'}
</span>
</td>
<td width='40%'>
<td width='39%'>
<div style={{ border: '2px dashed grey' }} className='n-p-1'>
<Dropdown
type='select'
Expand All @@ -354,7 +371,9 @@ export const NeoCustomReportActionsModal = ({
</td>

<td width='5%' className='n-text-center'>
<span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>TO</span>
<span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>
{!ruleTrigger.multiple ? 'TO' : 'WITH'}
</span>
</td>
<td width='20%'>
<div style={{ border: '2px dashed grey' }} className='n-p-1'>
Expand Down
3 changes: 3 additions & 0 deletions src/extensions/query-translator/QueryTranslatorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ClientSettingEntry {
label: string;
type: SELECTION_TYPES;
default: any;
password?: boolean;
authentication?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate
hasAuthButton?: boolean; // Append a button at the end of the selector to trigger an auth request.
methodFromClient?: string; // String that contains the name of the client function to call to retrieve the data needed to fill the option
Expand Down Expand Up @@ -47,6 +48,7 @@ export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = {
label: 'OpenAI API Key',
type: SELECTION_TYPES.TEXT,
default: '',
password: true,
hasAuthButton: true,
authentication: true,
},
Expand Down Expand Up @@ -74,6 +76,7 @@ export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = {
label: 'Subscription Key',
type: SELECTION_TYPES.TEXT,
default: '',
password: true,
hasAuthButton: true,
authentication: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const ClientSettings = ({
name={setting}
value={localSettings[setting]}
disabled={disabled}
password={defaultSettings[setting].password}
type={defaultSettings[setting].type}
label={defaultSettings[setting].label}
defaultValue={defaultSettings[setting].default}
Expand Down

0 comments on commit 1fbf33a

Please sign in to comment.