console: Add select all columns option while selecting the columns in event triggers

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2688
Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com>
GitOrigin-RevId: 0b98d2f388c7f8428f3c282e8c1662f8b046f4dd
This commit is contained in:
Varun Choudhary 2021-11-29 17:58:49 +05:30 committed by hasura-bot
parent 3fc34aa028
commit bc467a283a
13 changed files with 194 additions and 48 deletions

View File

@ -9,6 +9,7 @@
- server: fix to allow remote schema response to contain an "extensions" field (#7143)
- server: support database-to-database joins with BigQuery
- console: add comments to tracked functions
- console: add select all columns option while selecting the columns in event triggers
- console: add request transforms for events
- metadata SDK: add type definitions for config v3
- cli: fix cli-console failing to add migrations if there are tabs in SQL body (#7362)

View File

@ -15,9 +15,9 @@ import {
import {
setMetaData,
validateCT,
validateCTrigger,
validateInsert,
ResultType,
validateCTrigger,
} from '../../validators/validators';
import { setPromptValue } from '../../../helpers/common';
import {
@ -55,7 +55,7 @@ const statements = {
`,
};
const createETForm = () => {
const createETForm = (allCols: boolean) => {
// Set trigger name and select table
cy.getBySel('trigger-name').clear().type(getTriggerName(0, testName));
cy.getBySel('select-source').select('default');
@ -73,6 +73,10 @@ const createETForm = () => {
// advanced settings
cy.getBySel('advanced-settings').click();
if (!allCols) {
cy.getBySel('choose-column').click();
cy.getBySel('select-column').first().click();
}
// retry configuration
cy.getBySel('no-of-retries').clear().type(getNoOfRetries());
@ -139,8 +143,10 @@ export const failCTWithoutData = () => {
validateCT(getTriggerName(0, testName), ResultType.FAILURE);
};
export const passCT = () => {
createETForm();
export const passCT1 = () => {
// select choose column from the radio input
const allCols = false;
createETForm(allCols);
// Click on create
cy.getBySel('trigger-create').click();
@ -159,7 +165,36 @@ export const passCT = () => {
getTriggerName(0, testName),
getTableName(0, testName),
'public',
ResultType.SUCCESS
ResultType.SUCCESS,
allCols
);
};
export const passCT2 = () => {
cy.getBySel('data-sidebar-add').click();
// select all columns from the radio input
const allCols = true;
createETForm(allCols);
// Click on create
cy.getBySel('trigger-create').click();
cy.wait(10000);
// Check if the trigger got created and navigated to processed events page
cy.url().should(
'eq',
`${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(
0,
testName
)}/modify`
);
cy.getBySel(getTriggerName(0, testName));
// Validate
validateCTrigger(
getTriggerName(0, testName),
getTableName(0, testName),
'public',
ResultType.SUCCESS,
allCols
);
};
@ -272,7 +307,7 @@ export const createEtTransform = () => {
cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`);
// fill up the basic event trigger form
createETForm();
createETForm(true);
// open request transform section
toggleRequestTransformSection();

View File

@ -6,12 +6,13 @@ import { setMetaData } from '../../validators/validators';
import {
checkCreateTriggerRoute,
failCTWithoutData,
passCT,
passCT2,
failCTDuplicateTrigger,
insertTableRow,
deleteCTTestTrigger,
deleteCTTestTable,
passPTCreateTable,
passCT1,
createEtTransform,
modifyEtTransform,
deleteEtTransform,
@ -37,7 +38,8 @@ export const runCreateTriggerTests = () => {
checkCreateTriggerRoute
);
it('Fails to create trigger without data', failCTWithoutData);
it('Successfuly creates trigger', passCT);
it('Successfuly creates trigger with selected columns for update', passCT1);
it('Successfuly creates trigger with all columns for update', passCT2);
it('Fails to create duplicate trigger', failCTDuplicateTrigger);
it('Insert a row and invoke trigger', insertTableRow);
it("Delete's the test trigger", deleteCTTestTrigger);

View File

@ -475,7 +475,8 @@ export const validateCTrigger = (
triggerName: string,
tableName: string,
schemaName = 'public',
result: ResultType
result: ResultType,
allCols?: boolean
) => {
const reqBody = {
type: 'export_metadata',
@ -505,7 +506,11 @@ export const validateCTrigger = (
expect(response.status === 200).to.be.true;
expect(trigger.definition.insert.columns === '*').to.be.true;
expect(trigger.definition.delete.columns === '*').to.be.true;
expect(trigger.definition.update.columns.length === 3).to.be.true;
if (allCols) {
expect(trigger.definition.update.columns === '*').to.be.true;
} else {
expect(trigger.definition.update.columns.length === 2).to.be.true;
}
expect(
trigger.retry_conf.interval_sec === parseInt(getIntervalSeconds(), 10)
).to.be.true;

View File

@ -361,6 +361,7 @@ const Add: React.FC<Props> = props => {
state={state}
databaseInfo={databaseInfo}
dataSourcesList={dataSourcesList}
readOnlyMode={readOnlyMode}
handleTriggerNameChange={handleTriggerNameChange}
handleWebhookValueChange={handleWebhookValueChange}
handleWebhookTypeChange={handleWebhookTypeChange}
@ -371,6 +372,7 @@ const Add: React.FC<Props> = props => {
handleOperationsColumnsChange={handleOperationsColumnsChange}
handleRetryConfChange={handleRetryConfChange}
handleHeadersChange={handleHeadersChange}
handleToggleAllColumn={setState.toggleAllColumnChecked}
/>
<ConfigureTransformation
webhookUrl={webhook?.value}

View File

@ -22,6 +22,7 @@ type CreateETFormProps = {
state: LocalEventTriggerState;
databaseInfo: DatabaseInfo;
dataSourcesList: DataSource[];
readOnlyMode: boolean;
handleTriggerNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleWebhookValueChange: (v: string) => void;
handleWebhookTypeChange: (e: React.BaseSyntheticEvent) => void;
@ -32,6 +33,7 @@ type CreateETFormProps = {
handleOperationsColumnsChange: (oc: ETOperationColumn[]) => void;
handleRetryConfChange: (r: RetryConf) => void;
handleHeadersChange: (h: Header[]) => void;
handleToggleAllColumn: () => void;
};
const CreateETForm: React.FC<CreateETFormProps> = props => {
@ -45,9 +47,11 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
retryConf,
operations,
operationColumns,
isAllColumnChecked,
},
databaseInfo,
dataSourcesList,
readOnlyMode,
handleTriggerNameChange,
handleDatabaseChange,
handleSchemaChange,
@ -58,6 +62,7 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
handleOperationsColumnsChange,
handleRetryConfChange,
handleHeadersChange,
handleToggleAllColumn,
} = props;
const supportedDrivers = getSupportedDrivers('events.triggers.add');
@ -195,6 +200,9 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
<ColumnList
operationColumns={operationColumns}
table={table}
isAllColumnChecked={isAllColumnChecked}
readOnlyMode={readOnlyMode}
handleToggleAllColumn={handleToggleAllColumn}
handleOperationsColumnsChange={handleOperationsColumnsChange}
/>
</div>

View File

@ -2,15 +2,26 @@ import React from 'react';
import { QualifiedTable } from '@/metadata/types';
import styles from '../TableCommon/EventTable.scss';
import { ETOperationColumn } from '../../types';
import { ColumnSelectionRadioButton } from './ColumnSelectionRadioButton';
type ColumnListProps = {
operationColumns: ETOperationColumn[];
table: QualifiedTable;
isAllColumnChecked: boolean;
readOnlyMode: boolean;
handleToggleAllColumn: () => void;
handleOperationsColumnsChange: (oc: ETOperationColumn[]) => void;
};
const ColumnList: React.FC<ColumnListProps> = props => {
const { operationColumns, table, handleOperationsColumnsChange } = props;
const {
operationColumns,
table,
isAllColumnChecked,
readOnlyMode,
handleToggleAllColumn,
handleOperationsColumnsChange,
} = props;
if (!table.name) {
return <i>Select a table first to get column list</i>;
@ -28,24 +39,37 @@ const ColumnList: React.FC<ColumnListProps> = props => {
return (
<>
{operationColumns.map(opCol => (
<div
key={opCol.name}
className={`${styles.padd_remove} ${styles.columnListElement}`}
>
<div className="checkbox ">
<label className={styles.cursorPointer}>
<input
type="checkbox"
checked={opCol.enabled}
onChange={() => handleToggleColumn(opCol)}
className={`${styles.cursorPointer} legacy-input-fix`}
/>
{opCol.name}
</label>
</div>
</div>
))}
<ColumnSelectionRadioButton
isAllColumnChecked={isAllColumnChecked}
handleColumnRadioButton={handleToggleAllColumn}
readOnly={readOnlyMode}
/>
<div className="mt-sm">
{!isAllColumnChecked ? (
<>
<div>List of columns to select:</div>
{operationColumns.map(opCol => (
<div
key={opCol.name}
className={`${styles.padd_remove} ${styles.columnListElement}`}
>
<div className="checkbox">
<label className="cursor-pointer">
<input
type="checkbox"
checked={opCol.enabled}
onChange={() => handleToggleColumn(opCol)}
className={`${styles.cursorPointer} legacy-input-fix`}
data-test="select-column"
/>
{opCol.name}
</label>
</div>
</div>
))}
</>
) : null}
</div>
</>
);
};

View File

@ -0,0 +1,41 @@
import React from 'react';
interface InputProps extends React.ComponentProps<'input'> {
isAllColumnChecked: boolean;
handleColumnRadioButton: () => void;
readOnly: boolean;
}
export const ColumnSelectionRadioButton: React.FC<InputProps> = ({
isAllColumnChecked,
handleColumnRadioButton,
readOnly,
}) => {
return (
<div className="mt-sm">
<label className="radio-inline">
<input
type="radio"
checked={isAllColumnChecked}
onChange={handleColumnRadioButton}
disabled={readOnly}
readOnly
className="cursor-pointer"
/>
All columns
</label>
<label className="radio-inline">
<input
type="radio"
checked={!isAllColumnChecked}
onChange={handleColumnRadioButton}
disabled={readOnly}
readOnly
className="cursor-pointer"
data-test="choose-column"
/>
Choose columns
</label>
</div>
);
};

View File

@ -326,6 +326,8 @@ const Modify: React.FC<Props> = props => {
setOperationColumns={setState.operationColumns}
styles={styles}
save={saveWrapper('ops')}
isAllColumnChecked={state.isAllColumnChecked}
handleColumnRadioButton={setState.toggleAllColumnChecked}
/>
<RetryConfEditor
conf={state.retryConf}

View File

@ -13,6 +13,7 @@ import {
import Operations from '../Common/Operations';
import { TableColumn } from '../../../../../dataSources/types';
import { ColumnSelectionRadioButton } from '../Common/ColumnSelectionRadioButton';
type OperationEditorProps = {
currentTrigger: EventTrigger;
@ -23,6 +24,8 @@ type OperationEditorProps = {
setOperationColumns: (operationColumns: ETOperationColumn[]) => void;
styles: Record<string, string>;
save: (success: VoidCallback, error: VoidCallback) => void;
isAllColumnChecked: boolean;
handleColumnRadioButton: () => void;
};
const OperationEditor = (props: OperationEditorProps) => {
@ -35,8 +38,9 @@ const OperationEditor = (props: OperationEditorProps) => {
operationColumns,
setOperations,
setOperationColumns,
isAllColumnChecked,
handleColumnRadioButton,
} = props;
const etDef = currentTrigger.configuration.definition;
const existingOps = parseEventTriggerOperations(etDef);
const existingOpColumns = getETOperationColumns(
@ -73,7 +77,7 @@ const OperationEditor = (props: OperationEditorProps) => {
</div>
<div>
<label className="block text-gray-600 font-medium mb-xs">
Trigger Tables
Trigger Columns
</label>
<p className="text-sm text-gray-600 mb-sm">
Trigger columns to list to for updates.
@ -81,8 +85,25 @@ const OperationEditor = (props: OperationEditorProps) => {
</div>
<div className={styles.modifyOpsCollapsedContent}>
<div className={`col-md-12 ${styles.padd_remove}`}>
{ops.update ? (
opCols.map(col => {
<div className="checkbox ">
{ops.update ? (
<ColumnSelectionRadioButton
isAllColumnChecked={isAllColumnChecked}
handleColumnRadioButton={handleColumnRadioButton}
readOnly={readOnly}
/>
) : (
<div
className={`col-md-12 ${styles.padd_remove} ${styles.modifyOpsCollapsedtitle}`}
>
<i>Applicable only if update operation is selected.</i>
</div>
)}
</div>
</div>
{!isAllColumnChecked ? (
<div className={`col-md-12 ${styles.padd_remove}`}>
{opCols.map(col => {
const toggle = () => {
if (!readOnly) {
const newCols = opCols.map(oc => {
@ -111,15 +132,9 @@ const OperationEditor = (props: OperationEditorProps) => {
<small className={styles.addPaddSmall}> ({col.type})</small>
</label>
);
})
) : (
<div
className={`col-md-12 ${styles.padd_remove} ${styles.modifyOpsCollapsedtitle}`}
>
<i>Applicable only if update operation is selected.</i>
</div>
)}
</div>
})}
</div>
) : null}
</div>
</div>
);

View File

@ -27,6 +27,7 @@ export type LocalEventTriggerState = {
retryConf: RetryConf;
headers: Header[];
source: string;
isAllColumnChecked: boolean;
};
const defaultState: LocalEventTriggerState = {
@ -53,6 +54,7 @@ const defaultState: LocalEventTriggerState = {
},
headers: [defaultHeader],
source: '',
isAllColumnChecked: true,
};
export const parseServerETDefinition = (
@ -86,6 +88,7 @@ export const parseServerETDefinition = (
webhook: parseServerWebhook(etConf.webhook, etConf.webhook_from_env),
retryConf: etConf.retry_conf,
headers: parseServerHeaders(eventTrigger.configuration.headers),
isAllColumnChecked: etDef.update.columns === '*',
};
};
@ -161,6 +164,12 @@ export const useEventTrigger = (initState?: LocalEventTriggerState) => {
bulk: (s: LocalEventTriggerState) => {
setState(s);
},
toggleAllColumnChecked: () => {
setState(s => ({
...s,
isAllColumnChecked: !s.isAllColumnChecked,
}));
},
},
};
};

View File

@ -422,9 +422,11 @@ export const modifyEventTrigger = (
insert: state.operations.insert ? { columns: '*' } : null,
update: state.operations.update
? {
columns: state.operationColumns
.filter(c => !!c.enabled)
.map(c => c.name),
columns: state.isAllColumnChecked
? '*'
: state.operationColumns
.filter(c => !!c.enabled)
.map(c => c.name),
}
: null,
delete: state.operations.delete ? { columns: '*' } : null,

View File

@ -450,9 +450,9 @@ export const generateCreateEventTriggerQuery = (
: null,
update: state.operations.update
? {
columns: state.operationColumns
.filter(c => !!c.enabled)
.map(c => c.name),
columns: state.isAllColumnChecked
? '*'
: state.operationColumns.filter(c => !!c.enabled).map(c => c.name),
}
: null,
delete: state.operations.delete