console: fix visiting view modify page overwriting raw sql content (close #4798) (#4810)

This commit is contained in:
soorajshankar 2020-05-29 14:11:02 +05:30 committed by GitHub
parent bc16668ff6
commit b2fd57a8e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 181 additions and 159 deletions

View File

@ -12,23 +12,20 @@ It works similar to table relationships. Head to the `Relationship` tab in your
2. select the remote schema
3. give the join configuration from table columns to remote schema fields.
[Add docs links]
[Add console screenshot]
[Add docs links][add console screenshot]
### Scheduled Triggers
A scheduled trigger can be used to execute custom business logic based on time. There are two types of timing events: cron based or timestamp based.
A cron trigger will be useful when something needs to be done periodically. For example, you can create a cron trigger to generate an end-of-day sales report every weekday at 9pm.
A cron trigger will be useful when something needs to be done periodically. For example, you can create a cron trigger to generate an end-of-day sales report every weekday at 9pm.
You can also schedule one-off events based on a timestamp. For example, a new scheduled event can be created for 2 weeks from when a user signs up to send them an email about their experience.
[Add docs links]
[Add console screenshot]
[Add docs links][add console screenshot]
(close #1914)
### Allow access to session variables by computed fields (fix #3846)
Sometimes it is useful for computed fields to have access to the Hasura session variables directly. For example, suppose you want to fetch some articles but also get related user info, say `likedByMe`. Now, you can define a function like:
@ -63,6 +60,7 @@ Read more about the session argument for computed fields in the [docs](https://h
### Bug fixes and improvements
(Add entries here in the order of: server, console, cli, docs, others)
- server: fix explain queries with role permissions (fix #4816)
- server: compile with GHC 8.10.1, closing a space leak with subscriptions. (close #4517) (#3388)
@ -81,6 +79,7 @@ Read more about the session argument for computed fields in the [docs](https://h
- console: fix inconsistency between selected rows state and displayed rows (fix #4654) (#4673)
- console: fix displaying boolean values in `Edit Row` tab (#4682)
- console: fix underscores not being displayed on raw sql page (close #4754) (#4799)
- console: fix visiting view modify page overwriting raw sql content (fix #4798) (#4810)
- console: add help button and move about page to settings (#4848)
- cli: list all available commands in root command help (fix #4623) (#4628)
- docs: add section on actions vs. remote schemas to actions documentation (#4284)

View File

@ -14,63 +14,67 @@ export const openRawSQL = () => {
// Match URL
cy.url().should('eq', `${baseUrl}/data/sql`);
};
const clearText = () => {
cy.get('textarea').type('{selectall}', { force: true });
cy.get('textarea').trigger('keydown', {
keyCode: 46,
which: 46,
force: true,
});
cy.wait(2000); // ace editor textarea doesn't expose the value to check, so wait
};
export const passCreateTable = () => {
prevStr = 'CREATE TABLE Apic_test_table_rsql (id serial PRIMARY KEY);';
cy.get('textarea').type(prevStr, { force: true });
cy.wait(1000); // debounce
cy.get(getElementFromAlias('run-sql')).click();
cy.wait(5000);
};
export const passInsertValues = () => {
for (let i = 0; i < prevStr.length; i++) {
cy.get('textarea').type('{backspace}', { force: true });
}
clearText();
prevStr = 'INSERT INTO Apic_test_table_rsql VALUES (1);';
cy.get('textarea').type(prevStr, { force: true });
cy.wait(1000);
cy.get(getElementFromAlias('run-sql')).click();
cy.wait(5000);
};
export const passAlterTable = () => {
for (let i = 0; i < prevStr.length; i++) {
cy.get('textarea').type('{backspace}', { force: true });
}
clearText();
prevStr = 'ALTER TABLE Apic_test_table_rsql ADD COLUMN name text;';
cy.get('textarea').type(prevStr, { force: true });
// Untrack table
cy.wait(1000);
cy.get(getElementFromAlias('raw-sql-track-check')).uncheck();
cy.get(getElementFromAlias('run-sql')).click();
cy.wait(5000);
};
export const passCreateView = () => {
for (let i = 0; i < prevStr.length; i++) {
cy.get('textarea').type('{backspace}', { force: true });
}
clearText();
prevStr = 'CREATE VIEW abcd AS SELECT * FROM Apic_test_table_rsql;';
cy.get('textarea').type(prevStr, { force: true });
// Track table
cy.wait(1000);
cy.get(getElementFromAlias('raw-sql-track-check')).check();
cy.get(getElementFromAlias('run-sql')).click();
cy.wait(5000);
};
export const delTestTables = () => {
for (let i = 0; i < prevStr.length; i++) {
cy.get('textarea').type('{backspace}', { force: true });
}
clearText();
prevStr = 'DROP TABLE Apic_test_table_rsql CASCADE;';
cy.get('textarea').type(prevStr, { force: true });
cy.wait(1000);
cy.get(getElementFromAlias('raw-sql-migration-check')).uncheck();
cy.get(getElementFromAlias('run-sql')).click();
cy.get(getElementFromAlias('not-migration-confirm')).click();
cy.wait(5000);
for (let i = 0; i < prevStr.length; i++) {
cy.get('textarea').type('{backspace}', { force: true });
}
prevStr = 'DROP TABLE abcd;';
cy.get('textarea').type(prevStr, { force: true });
cy.get(getElementFromAlias('run-sql')).click();
cy.wait(5000);
// cy.visit(`${baseUrl}/data/schema/public`);
// cy.get(getElementFromAlias('add-track-table-Apic_test_table_rsql')).click();
// cy.get(getElementFromAlias('delete-table')).click();
// cy.on('window:confirm', () => true);
// cy.wait(5000);
// validateCT('Apic_test_table_rsql', 'failure');
};

View File

@ -0,0 +1,16 @@
import React from 'react';
export type AlertType = 'warning' | 'danger' | 'success';
interface AlertProps {
type: AlertType;
text: string;
}
const Alert: React.FC<AlertProps> = ({ type, text }) => (
<div className={`hidden alert alert-${type}`} role="alert">
{text}
</div>
);
export default Alert;

View File

@ -12,7 +12,7 @@ import styles from '../Common.scss';
export interface ButtonProps extends React.ComponentProps<'button'> {
size: string;
color: 'yellow' | 'red' | 'green' | 'gray' | 'white' | 'black';
color?: 'yellow' | 'red' | 'green' | 'gray' | 'white' | 'black';
}
const Button: React.FC<ButtonProps> = props => {

View File

@ -142,6 +142,7 @@ const defaultModifyState = {
lastSuccess: null,
viewDefinition: null,
viewDefinitionError: null,
viewDefSql: '',
tableCommentEdit: { enabled: false, editedValue: null },
alterColumnOptions: [], // Store supported implicit column -> column casts
alterColumnOptionsFetchErr: null,

View File

@ -24,7 +24,31 @@ import {
ACE_EDITOR_FONT_SIZE,
} from '../../../Common/AceEditor/utils';
import { CLI_CONSOLE_MODE } from '../../../../constants';
import NotesSection from './molecules/NotesSection';
import Alert from '../../../Common/Alert';
/**
* # RawSQL React FC
* ## renders raw SQL page on route `/data/sql`
*
* @typedef Props
* @property {string} sql
* @property {string} resultType
* @property {array} result
* @property {array} resultHeaders
* @property {function} dispatch
* @property {boolean} ongoingRequest
* @property {object} lastError
* @property {boolean} lastSuccess
* @property {boolean} isModalOpen
* @property {boolean} isCascadeChecked
* @property {boolean} isMigrationChecked
* @property {boolean} isTableTrackChecked
* @property {boolean} migrationMode
* @property {array} allSchemas
*
* @param {Props}
*/
const RawSQL = ({
sql,
resultType,
@ -51,7 +75,6 @@ const RawSQL = ({
// set up sqlRef to use in unmount
const sqlRef = useRef(sql);
// set SQL from localStorage on mount and write back to localStorage on unmount
useEffect(() => {
if (!sql) {
const sqlFromLocalStorage = localStorage.getItem(LS_RAW_SQL_SQL);
@ -59,12 +82,10 @@ const RawSQL = ({
dispatch({ type: SET_SQL, data: sqlFromLocalStorage });
}
}
return () => {
localStorage.setItem(LS_RAW_SQL_SQL, sqlRef.current);
};
}, []);
// set SQL to sqlRef
useEffect(() => {
sqlRef.current = sql;
@ -127,34 +148,6 @@ const RawSQL = ({
}
};
let alert = null;
if (ongoingRequest) {
alert = (
<div className={`${styles.padd_left_remove} col-xs-12`}>
<div className="hidden alert alert-warning" role="alert">
Running...
</div>
</div>
);
} else if (lastError) {
alert = (
<div className={`${styles.padd_left_remove} col-xs-12`}>
<div className="hidden alert alert-danger" role="alert">
Error: {JSON.stringify(lastError)}
</div>
</div>
);
} else if (lastSuccess) {
alert = (
<div className={`${styles.padd_left_remove} col-xs-12`}>
<div className="hidden alert alert-success" role="alert">
Executed Query
</div>
</div>
);
}
const getMigrationWarningModal = () => {
const onModalClose = () => {
dispatch(modalClose());
@ -256,6 +249,8 @@ const RawSQL = ({
},
]}
onChange={handleSQLChange}
// prevents unwanted frequent event triggers
debounceChangePeriod={200}
/>
</div>
);
@ -305,25 +300,6 @@ const RawSQL = ({
return resultTable;
};
const getNotesSection = () => {
return (
<ul>
<li>
You can create views, alter tables or just about run any SQL
statements directly on the database.
</li>
<li>
Multiple SQL statements can be separated by semicolons, <code>;</code>
, however, only the result of the last SQL statement will be returned.
</li>
<li>
Multiple SQL statements will be run as a transaction. i.e. if any
statement fails, none of the statements will be applied.
</li>
</ul>
);
};
const getMetadataCascadeSection = () => {
return (
<div className={styles.add_mar_top_small}>
@ -426,15 +402,7 @@ const RawSQL = ({
<div>
<label className={styles.add_mar_right}>Migration name:</label>
<input
className={
styles.inline_block +
' ' +
styles.tableNameInput +
' ' +
styles.add_mar_right_small +
' ' +
' form-control'
}
className={`${styles.inline_block} ${styles.tableNameInput} ${styles.add_mar_right_small} form-control`}
placeholder={'run_sql_migration'}
id="migration-name"
type="text"
@ -473,21 +441,6 @@ const RawSQL = ({
return migrationSection;
};
const getRunButton = () => {
return (
<Button
type="submit"
className={styles.add_mar_top}
onClick={submitSQL}
color="yellow"
size="sm"
data-test="run-sql"
>
Run!
</Button>
);
};
return (
<div
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top}`}
@ -500,26 +453,44 @@ const RawSQL = ({
<div className="clearfix" />
</div>
<div className={styles.add_mar_top}>
<div>
<div className={`${styles.padd_left_remove} col-xs-8`}>
{getNotesSection()}
</div>
<div className={`${styles.padd_left_remove} col-xs-8`}>
<NotesSection />
</div>
<div className={`${styles.padd_left_remove} col-xs-10`}>
{getSQLSection()}
</div>
<div className={`${styles.padd_left_remove} col-xs-10`}>
{getSQLSection()}
</div>
<div
className={`${styles.padd_left_remove} ${styles.add_mar_bottom} col-xs-8`}
<div
className={`${styles.padd_left_remove} ${styles.add_mar_bottom} col-xs-8`}
>
{getTrackThisSection()}
{getMetadataCascadeSection()}
{getMigrationSection()}
<Button
type="submit"
className={styles.add_mar_top}
onClick={submitSQL}
color="yellow"
size="sm"
data-test="run-sql"
>
{getTrackThisSection()}
{getMetadataCascadeSection()}
{getMigrationSection()}
Run!
</Button>
</div>
{getRunButton()}
<div className="hidden col-xs-4">
<div className={`${styles.padd_left_remove} col-xs-12`}>
{ongoingRequest && <Alert type="warning" text="Running..." />}
{lastError && (
<Alert
type="danger"
text={`Error: ${JSON.stringify(lastError)}`}
/>
)}
{lastSuccess && <Alert type="success" text="Executed Query" />};
</div>
</div>
<div className="hidden col-xs-4">{alert}</div>
</div>
{getMigrationWarningModal()}

View File

@ -0,0 +1,22 @@
import React from 'react';
const NotesSection = () => {
return (
<ul>
<li>
You can create views, alter tables or just about run any SQL statements
directly on the database.
</li>
<li>
Multiple SQL statements can be separated by semicolons, <code>;</code>,
however, only the result of the last SQL statement will be returned.
</li>
<li>
Multiple SQL statements will be run as a transaction. i.e. if any
statement fails, none of the statements will be applied.
</li>
</ul>
);
};
export default NotesSection;

View File

@ -23,6 +23,7 @@ import {
TOGGLE_ENUM_FAILURE,
MODIFY_ROOT_FIELD,
SET_CHECK_CONSTRAINTS,
SET_VIEW_DEF_SQL,
} from '../TableModify/ModifyActions';
// TABLE RELATIONSHIPS
@ -119,6 +120,11 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
...modifyState,
viewDefinitionError: action.data,
};
case SET_VIEW_DEF_SQL:
return {
...modifyState,
viewDefSql: action.data,
};
case REL_ADD_NEW_CLICKED:
return {

View File

@ -8,7 +8,6 @@ import {
LOAD_SCHEMA,
} from '../DataActions';
import _push from '../push';
import { SET_SQL } from '../RawSQL/Actions';
import {
showErrorNotification,
showSuccessNotification,
@ -73,6 +72,7 @@ const DELETE_PK_WARNING =
const VIEW_DEF_REQUEST_SUCCESS = 'ModifyTable/VIEW_DEF_REQUEST_SUCCESS';
const VIEW_DEF_REQUEST_ERROR = 'ModifyTable/VIEW_DEF_REQUEST_ERROR';
const SET_VIEW_DEF_SQL = 'ModifyTable/SET_VIEW_DEF_SQL';
const SAVE_NEW_TABLE_NAME = 'ModifyTable/SAVE_NEW_TABLE_NAME';
@ -967,7 +967,7 @@ WHERE c.relname = '${viewName}'
' AS \n' +
finalDef;
}
dispatch({ type: SET_SQL, data: runSqlDef });
dispatch({ type: SET_VIEW_DEF_SQL, data: runSqlDef });
},
err => {
dispatch(
@ -2369,6 +2369,7 @@ export {
FETCH_COLUMN_TYPE_CASTS_FAIL,
VIEW_DEF_REQUEST_SUCCESS,
VIEW_DEF_REQUEST_ERROR,
SET_VIEW_DEF_SQL,
SET_COLUMN_EDIT,
TABLE_COMMENT_EDIT,
TABLE_COMMENT_INPUT_EDIT,

View File

@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import AceEditor from 'react-ace';
import TableHeader from '../TableCommon/TableHeader';
import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor';
import {
@ -28,10 +27,11 @@ import RootFields from './RootFields';
import Tooltip from '../../../Common/Tooltip/Tooltip';
import { changeViewRootFields } from '../Common/TooltipMessages';
import styles from './ModifyTable.scss';
import ViewDefinitions from './ViewDefinitions';
const ModifyView = props => {
const {
sql,
viewDefSql,
tableName,
tableType,
allSchemas,
@ -98,12 +98,6 @@ const ModifyView = props => {
);
}
const modifyViewDefinition = viewName => {
// fetch the definition
dispatch(fetchViewDefinition(viewName, true));
// redirect the user to run_sql page and set state
};
const getViewColumnsSection = () => {
const columns = tableSchema.columns.sort(ordinalColSort);
@ -200,21 +194,6 @@ const ModifyView = props => {
);
};
const modifyViewOnClick = () => {
modifyViewDefinition(tableName);
};
const modifyBtn = (
<Button
type="submit"
size="xs"
className={styles.add_mar_right}
onClick={modifyViewOnClick}
data-test="modify-view"
>
Modify
</Button>
);
const untrackOnclick = () => {
const confirmMessage = `This will remove the view "${tableName}" from the GraphQL schema`;
const isOk = getConfirmation(confirmMessage);
@ -276,21 +255,8 @@ const ModifyView = props => {
<h4 className={styles.subheading_text}>Columns</h4>
{getViewColumnsSection()}
<br />
<h4 className={styles.subheading_text}>
View Definition:
<span className={styles.add_mar_left}>{modifyBtn}</span>
</h4>
<AceEditor
mode="sql"
theme="github"
value={sql}
name="raw_sql"
minLines={8}
maxLines={100}
width="100%"
showPrintMargin={false}
readOnly
/>
<ViewDefinitions dispatch={dispatch} sql={viewDefSql} />
<hr />
{getViewRootFieldsSection()}
{untrackBtn}
@ -342,7 +308,6 @@ const mapStateToProps = (state, ownProps) => {
tableType,
currentSchema: schemaName,
allSchemas: state.tables.allSchemas,
sql: state.rawSQL.sql,
migrationMode: state.main.migrationMode,
readOnlyMode: state.main.readOnlyMode,
serverVersion: state.main.serverVersion,

View File

@ -0,0 +1,35 @@
import React from 'react';
import styles from './ModifyTable.scss';
import TextAreaWithCopy from '../../../Common/TextAreaWithCopy/TextAreaWithCopy';
import RawSqlButton from '../Common/Components/RawSqlButton';
export interface ViewDefinitionsProps {
dispatch: () => void;
sql: string | object;
}
const ViewDefinitions: React.FC<ViewDefinitionsProps> = ({ dispatch, sql }) => (
<>
<h4 className={styles.subheading_text}>
View Definition:
<span className={styles.add_mar_left}>
<RawSqlButton
className={styles.add_mar_right}
sql={sql}
dispatch={dispatch}
data-test="modify-view"
>
Modify
</RawSqlButton>
</span>
</h4>
<TextAreaWithCopy
copyText={sql}
textLanguage="sql"
id="copyCustomFunctionSQL"
/>
</>
);
export default ViewDefinitions;

View File

@ -21,9 +21,11 @@ export const LoadingSkeleton = () => {
);
return (
<div className={`${styles.schemaExplorerContainer} ${styles.overflowAuto}`}>
{Array(5).fill(null).map((_, i) => (
<div key={i}>{skeletonItem}</div>
))}
{Array(5)
.fill(null)
.map((_, i) => (
<div key={i}>{skeletonItem}</div>
))}
</div>
);
};