console: add reload all databases checkbox to the metadata settings page

Closes: https://github.com/hasura/graphql-engine/issues/7146

### Description

This PR adds a `reload all databases` check box along `with` the `reload metadata` check box to reload all databases (inconsistent databases also).

![Screenshot from 2021-07-07 16-51-49](https://user-images.githubusercontent.com/68095256/124750932-af31a000-df43-11eb-88d9-b6585214a188.png)

### Affected components

- [x] Console

### Steps to test and verify

Use this for your docker image: https://235669-308610159-gh.circle-artifacts.com/0/server/image.tar

1. Go to the metadata setting page
2. If `reload all databases` check box is checked: It reloads everything by sending * in the API
3. If `reloading all databases` check box is not checked: It reloads inconsistent DB and if no inconsistent then it passes an empty string

https://github.com/hasura/graphql-engine-mono/pull/1703

GitOrigin-RevId: 2b6f029a17d40a43f5bce3680d5681c97433d78c
This commit is contained in:
Varun Choudhary 2021-07-14 11:45:14 +05:30 committed by hasura-bot
parent cdfb71c1da
commit b1013e8d6c
9 changed files with 305 additions and 14 deletions

View File

@ -15,7 +15,8 @@
- server: log all HTTP errors in remote schema calls as `remote-schema-error` with details
- server: For BigQuery, make `global_select_limit` configuration optional with a default value of
`1000`
- console: add `reload all databases` checkbox to the metadata settings page
- console: add schema sharing
## v2.0.1

View File

@ -34,8 +34,8 @@ const MetadataOptions = props => {
<div key="meta_data_1" className={styles.intro_note}>
<h4>Reload metadata</h4>
<div className={styles.content_width}>
Refresh Hasura metadata, typically required if you have changed the
underlying database or if you have updated your remote schemas.
Refresh Hasura metadata, typically required if you have changes in
the underlying databases or if you have updated your remote schemas.
</div>
</div>

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '../../../Common/Button/Button';
import Button from '../../../Common/Button/Button';
import {
showSuccessNotification,
showErrorNotification,
@ -10,12 +10,15 @@ import Tooltip from '../../../Common/Tooltip/Tooltip';
import metaDataStyles from '../Settings.scss';
import { reloadMetadata } from '../../../../metadata/actions';
import styles from '../Settings.scss';
class ReloadMetadata extends Component {
constructor(props) {
super(props);
this.state = {
isReloading: false,
shouldReloadRemoteSchemas: props.shouldReloadRemoteSchemas || false,
shouldReloadRemoteSchemas: false,
shouldReloadAllSources: false,
};
}
@ -25,6 +28,12 @@ class ReloadMetadata extends Component {
}));
};
toggleShouldReloadAllSources = () => {
this.setState(state => ({
shouldReloadAllSources: !state.shouldReloadAllSources,
}));
};
render() {
const {
dispatch,
@ -32,7 +41,11 @@ class ReloadMetadata extends Component {
tooltipStyle,
showReloadRemoteSchemas = true,
} = this.props;
const { isReloading, shouldReloadRemoteSchemas } = this.state;
const {
isReloading,
shouldReloadRemoteSchemas,
shouldReloadAllSources,
} = this.state;
const reloadMetadataAndLoadInconsistentMetadata = e => {
e.preventDefault();
@ -42,6 +55,7 @@ class ReloadMetadata extends Component {
dispatch(
reloadMetadata(
shouldReloadRemoteSchemas,
shouldReloadAllSources,
() => {
dispatch(showSuccessNotification('Metadata reloaded'));
this.setState({ isReloading: false });
@ -90,6 +104,22 @@ class ReloadMetadata extends Component {
<Tooltip message="Check this if you have inconsistent remote schemas or if your remote schema has changed." />
</>
)}
<>
<label
className={`${metaDataStyles.cursorPointer} ${metaDataStyles.add_mar_right_small} ${metaDataStyles.add_mar_left_small} ${styles.metaDataMargin}`}
disabled={this.state.isReloading}
>
<input
type="checkbox"
onChange={this.toggleShouldReloadAllSources}
checked={shouldReloadAllSources}
readOnly
className={`${metaDataStyles.add_mar_right_small} ${metaDataStyles.cursorPointer}`}
/>
Reload all databases
</label>
<Tooltip message="Check this if you have inconsistent databases." />
</>
</>
);
}

View File

@ -109,4 +109,8 @@
.roleNameContainer{
padding-left: 15px;
}
.metaDataMargin {
margin-left: 15px;
}

View File

@ -1,6 +1,115 @@
import { HasuraMetadataV3 } from '../../types';
import { APILimitInputType } from '../../utils';
export const inconsistentSourceObjects = [
{
definition: 'TestDB',
reason: 'Inconsistent object: sql server exception',
name: 'source TestDB',
type: 'source',
},
];
export const inconsistentTableObject = [
{
definition: {
name: 'Table2',
schema: 'TestSchema',
},
name: 'table TestSchema.Table2 in source default',
reason: 'Inconsistent object: no such table/view exists in source',
type: 'table',
},
];
export const inconsistentRelationshipObject = [
{
definition: {
name: 'relationshipName',
source: 'default',
Comment: null,
},
name:
'object_relation relationshipName in table TestSchema.Table1 in source default',
reason: 'Inconsistent object: table "TestSchema.Table2" is not tracked',
type: 'object_relation',
},
];
export const inconsistentObject = [
{
definition: 'TestDB',
reason: 'Inconsistent object: sql server exception',
name: 'source TestDB',
type: 'source',
},
{
definition: {
name: 'Table2',
schema: 'TestSchema',
},
name: 'table TestSchema.Table2 in source default',
reason: 'Inconsistent object: no such table/view exists in source',
type: 'table',
},
{
definition: {
name: 'relationshipName',
source: 'default',
Comment: null,
},
name:
'object_relation relationshipName in table TestSchema.Table1 in source default',
reason: 'Inconsistent object: table "TestSchema.Table2" is not tracked',
type: 'object_relation',
},
];
export const inconsistentRemoteSchema = [
{
name: `remote_schema_permission role permission in remote schema Test1`,
reason: `Inconsistent object: remote schema "Test1" does not exis`,
type: 'remote_schema_permission',
},
{
name: `remote_schema Test2`,
reason: `Inconsistent object: Error in $: key "data" not found`,
type: `remote_schema`,
},
];
export const inconsistentRemoteRelation = [
{
definition: {
name: 'relation_name',
remote_schema: 'Test',
source: 'default',
},
type: 'remote_relationship',
},
];
export const multipleInconsistencyRemoteSchema = [
{
name: `remote_schema_permission role permission in remote schema Test`,
reason: `Inconsistent object: remote schema "Test" does not exis`,
type: 'remote_schema_permission',
},
{
name: `remote_schema Test`,
reason: `Inconsistent object: Error in $: key "data" not found`,
type: `remote_schema`,
},
{
definition: {
name: 'relation_name',
remote_schema: 'Test',
source: 'default',
},
type: 'remote_relationship',
},
];
type apiLimitsType = {
old_state: HasuraMetadataV3['api_limits'];
new_state: {

View File

@ -0,0 +1,59 @@
import {
getRemoteSchemaNameFromInconsistentObjects,
getSourceFromInconistentObjects,
} from '../utils';
import {
inconsistentObject,
inconsistentRelationshipObject,
inconsistentRemoteRelation,
inconsistentRemoteSchema,
inconsistentSourceObjects,
inconsistentTableObject,
multipleInconsistencyRemoteSchema,
} from './fixtures/input';
describe('Metadata_Utils_getSourceFromInconistentObjects()', () => {
it('should generate an array of soure name for inconsistent DB ', () => {
const sourceNameArray = getSourceFromInconistentObjects(
inconsistentSourceObjects
);
expect(sourceNameArray).toEqual(['TestDB']);
});
it('should generate an array of soure name from inconsistent object due to table deletion ', () => {
const sourceNameArray = getSourceFromInconistentObjects(
inconsistentTableObject
);
expect(sourceNameArray).toEqual([]);
});
it('should generate an array of soure name from inconsistent object due to inconsistent relation ', () => {
const sourceNameArray = getSourceFromInconistentObjects(
inconsistentRelationshipObject
);
expect(sourceNameArray).toEqual(['default']);
});
it('should generate an array of soure name from inconsistent object due to inconsistent relation, DB and table deletion ', () => {
const sourceNameArray = getSourceFromInconistentObjects(inconsistentObject);
expect(sourceNameArray).toEqual(['TestDB', 'default']);
});
});
describe('Metadata_Utils_getRemoteSchemaNameFromInconsistentObjects()', () => {
it('should generate an array of remote schema name for inconsistent remote_schema, remote_schema_permission, remote_schema_relation ', () => {
const rsNameArray = getRemoteSchemaNameFromInconsistentObjects(
inconsistentRemoteSchema
);
expect(rsNameArray).toEqual(['Test1', 'Test2']);
});
it('should generate an array of remote schema name for inconsistent remote_relation ', () => {
const rsNameArray = getRemoteSchemaNameFromInconsistentObjects(
inconsistentRemoteRelation
);
expect(rsNameArray).toEqual(['Test']);
});
it('should generate an array of a single remote schema name for multiple inconsistency in a remote_schema ', () => {
const rsNameArray = getRemoteSchemaNameFromInconsistentObjects(
multipleInconsistencyRemoteSchema
);
expect(rsNameArray).toEqual(['Test']);
});
});

View File

@ -27,6 +27,8 @@ import {
updateAllowedQueryQuery,
allowedQueriesCollection,
addAllowedQuery,
getSourceFromInconistentObjects,
getRemoteSchemaNameFromInconsistentObjects,
} from './utils';
import {
makeMigrationCall,
@ -671,19 +673,49 @@ export const loadInconsistentObjects = (
reloadConfig: {
shouldReloadMetadata?: boolean;
shouldReloadRemoteSchemas?: boolean;
shouldReloadAllSources?: boolean;
},
successCb?: () => void,
failureCb?: (error: string) => void
): Thunk<void, MetadataActions> => {
return (dispatch, getState) => {
const inconsistentObjectsInMetadata = getState().metadata
.inconsistentObjects;
const inconsistentSources = getSourceFromInconistentObjects(
inconsistentObjectsInMetadata
);
const inconsistentRemoteSchemas = getRemoteSchemaNameFromInconsistentObjects(
inconsistentObjectsInMetadata
);
const headers = getState().tables.dataHeaders;
const source = getState().tables.currentDataSource;
const { shouldReloadMetadata, shouldReloadRemoteSchemas } = reloadConfig;
const {
shouldReloadMetadata,
shouldReloadRemoteSchemas,
shouldReloadAllSources,
} = reloadConfig;
let reloadSources: string[] | boolean = [];
if (shouldReloadAllSources) {
reloadSources = true;
} else if (inconsistentSources.length) {
reloadSources = inconsistentSources;
}
let reloadRemoteSchemas: string[] | boolean = [];
if (shouldReloadRemoteSchemas) {
reloadRemoteSchemas = true;
} else if (inconsistentRemoteSchemas.length) {
reloadRemoteSchemas = inconsistentRemoteSchemas;
}
const loadQuery = shouldReloadMetadata
? getReloadCacheAndGetInconsistentObjectsQuery(
!!shouldReloadRemoteSchemas,
source
reloadRemoteSchemas,
source,
reloadSources
)
: inconsistentObjectsQuery;
@ -799,6 +831,7 @@ export const reloadRemoteSchema = (
export const reloadMetadata = (
shouldReloadRemoteSchemas: boolean,
shouldReloadAllSources: boolean,
successCb: () => void,
failureCb: () => void
): Thunk<void, MetadataActions> => {
@ -808,6 +841,7 @@ export const reloadMetadata = (
{
shouldReloadMetadata: true,
shouldReloadRemoteSchemas,
shouldReloadAllSources,
},
successCb,
failureCb

View File

@ -386,10 +386,14 @@ export const dropInconsistentObjectsQuery = {
args: {},
};
export const getReloadMetadataQuery = (shouldReloadRemoteSchemas: boolean) => ({
export const getReloadMetadataQuery = (
shouldReloadRemoteSchemas: boolean | string[],
shouldReloadSources?: boolean | string[]
) => ({
type: 'reload_metadata',
args: {
reload_remote_schemas: shouldReloadRemoteSchemas,
reload_sources: shouldReloadSources ?? [],
reload_remote_schemas: shouldReloadRemoteSchemas ?? [],
},
});

View File

@ -136,13 +136,14 @@ export const reloadRemoteSchemaCacheAndGetInconsistentObjectsQuery = (
};
export const getReloadCacheAndGetInconsistentObjectsQuery = (
shouldReloadRemoteSchemas: boolean,
source: string
shouldReloadRemoteSchemas: boolean | string[],
source: string,
shouldReloadSources?: boolean | string[]
) => ({
type: 'bulk',
source,
args: [
getReloadMetadataQuery(shouldReloadRemoteSchemas),
getReloadMetadataQuery(shouldReloadRemoteSchemas, shouldReloadSources),
inconsistentObjectsQuery,
],
});
@ -293,3 +294,52 @@ export const isMetadataEmpty = (metadataObject: HasuraMetadataV3) => {
export const hasSources = (metadataObject: HasuraMetadataV3) => {
return metadataObject?.sources?.length > 0;
};
// NOTE: for a inconsistent object of type "source" the inconsistentObject.definition is the name of the source
// for every other inconsistent object if "source" is relevent it will be in inconsistentObject.definition.source
// getSourceFromInconistentObjects should be used to extract the source from any inconsistent object
export const getSourceFromInconistentObjects = (inconsistentObjects: any[]) =>
inconsistentObjects
.map(
inconsistentObject =>
(inconsistentObject?.type === 'source' &&
inconsistentObject?.definition) ||
inconsistentObject?.definition?.source
)
.filter(sourceName => typeof sourceName === 'string')
.filter(
(sourceName, index, sourceNameList) =>
sourceNameList?.indexOf(sourceName) === index
); // to remove duplicate source names
// NOTE: It can be seen that the `name` field within the object that contains the
// information about the inconsistent object for `remote_schema` and `remote_schema_permission`
// contains a sentence with complete details of the remote schema(like name, role .etc). In here,
// the name of the remote schema always comes at the very end. Since this "HACK" is being
// used to fetch the remote schema name, it can become a source of bugs.
export const getRemoteSchemaNameFromInconsistentObjects = (
inconsistentObjects: any[]
) =>
inconsistentObjects.reduce((rsNameList, inconsistentObject) => {
const inconsistantObjectSplited = inconsistentObject?.name?.split(' ');
if (
inconsistentObject?.type === 'remote_schema' ||
inconsistentObject?.type === 'remote_schema_permission'
) {
const rsName =
inconsistantObjectSplited?.[inconsistantObjectSplited?.length - 1];
if (!rsNameList.includes(rsName)) {
// to avoid duplicate remote schema name
return [...rsNameList, rsName];
}
} else if (
inconsistentObject?.type === 'remote_relationship' &&
inconsistentObject?.definition?.remote_schema
) {
if (!rsNameList.includes(inconsistentObject?.definition?.remote_schema)) {
return [...rsNameList, inconsistentObject?.definition?.remote_schema];
}
}
return rsNameList;
}, []);