console: connect database enhancements and misc fixes

Co-authored-by: Aleksandra Sikora <9019397+beerose@users.noreply.github.com>
GitOrigin-RevId: a44482a1f88ad94c462b72162cbfbb35397640a3
This commit is contained in:
Sooraj 2021-03-12 01:11:05 +05:30 committed by hasura-bot
parent 64d52f5fa3
commit ec79fcf52a
12 changed files with 229 additions and 131 deletions

View File

@ -480,6 +480,9 @@ input {
padding-right: 0 !important;
}
.add_pad_min {
padding: 1em;
}
.add_pad_right {
padding-right: 15px;
}
@ -595,6 +598,9 @@ code {
.add_pad_top {
padding-top: 20px;
}
.add_pad_top_10 {
padding-top: 10px;
}
.padd_bottom {
padding-bottom: 10px !important;
@ -1436,14 +1442,13 @@ code {
}
.db_list_item {
display: flex;
margin-top: 20px;
border-bottom: 1px solid rgb(187, 187, 187);
padding-bottom: 12px;
border-bottom: 1px solid rgb(232, 232, 232);
padding-left: 0;
}
.db_list_item:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
.show_connection_string {
@ -1452,34 +1457,20 @@ code {
cursor: pointer;
}
color: #337ab7;
p {
margin-bottom: 0;
}
}
.showAdminSecret {
font-size: 16px;
}
.db_display_data {
.db_large_string_break_words {
max-width: 40%;
word-break: break-word;
padding: 1em;
display: flex;
align-items: flex-start;
padding-left: 95px;
flex-direction: column;
}
.db_list_content {
display: inline-flex;
width: 30%;
}
.data_list_container {
display: flex;
flex-direction: column;
}
.db_item_actions {
width: 14%;
display: flex;
margin-right: 10px;
align-items: center;
}
.text_red {
@ -1489,6 +1480,7 @@ code {
.connect_db_content {
display: flex;
flex-direction: column;
padding-left: 10px;
}
.connect_db_header {
@ -1511,13 +1503,12 @@ code {
}
.connect_db_radio_label {
margin-left: 4px;
margin-right: 24px;
}
.connect_form_layout {
width: 50%;
padding: 8px;
padding: 8px 0;
display: flex;
flex-direction: column;
margin-top: 10px;

View File

@ -97,9 +97,9 @@
display: flex;
align-items: center;
border-right: 1px solid #788095;
display: inline-block;
width: 20%;
min-width: 240px;
height: 54px;
.logoParent {
display: flex;
@ -1530,13 +1530,21 @@
}
}
@media (max-width: 1250px) {
@media (max-width: 150px) {
.secureSectionText {
display: none;
}
}
@media (max-width: 1050px) {
@media (max-width: 1350px) {
.sidebar {
.header_logo_wrapper {
padding: 0px 10px;
}
}
}
@media (max-width: 1250px) {
.sidebar {
.sidebarItems {
li {

View File

@ -16,7 +16,7 @@ import {
} from './DataActions';
import { currentDriver, useDataSource } from '../../../dataSources';
import SourceView from './SourceView';
import { getSourceDriver } from './utils';
import { getSourceDriver, isInconsistentSource } from './utils';
type Params = {
source?: string;
@ -35,10 +35,19 @@ const DataSourceContainer = ({
dispatch,
currentSource,
location,
inconsistentObjects,
}: DataSourceContainerProps) => {
const { setDriver } = useDataSource();
const [dataLoaded, setDataLoaded] = useState(false);
const { source, schema } = params;
useEffect(() => {
// if the source is inconsistent, do not show the source route
if (isInconsistentSource(currentSource, inconsistentObjects)) {
dispatch(push('/data/manage'));
}
}, [inconsistentObjects, currentSource, dispatch, location]);
useEffect(() => {
if (!source || source === 'undefined') {
if (currentSource) {
@ -109,6 +118,7 @@ const mapStateToProps = (state: ReduxState) => {
schemaList: state.tables.schemaList,
dataSources: getDataSources(state),
currentSource: state.tables.currentDataSource,
inconsistentObjects: state.metadata.inconsistentObjects,
};
};
const dataSourceConnector = connect(

View File

@ -1,4 +1,4 @@
import React, { ChangeEvent } from 'react';
import React, { ChangeEvent, FormEvent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Tabbed from './TabbedDataSourceConnection';
@ -103,12 +103,13 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
};
const onSuccessConnectDBCb = () => {
setLoading(false);
resetState();
// route to manage page
dispatch(_push('/data/manage'));
};
const onClickConnectDatabase = () => {
const onConnectDatabase = () => {
if (!connectDBInputState.displayName.trim()) {
dispatch(
showErrorNotification(
@ -204,10 +205,14 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
.then(() => setLoading(false))
.catch(() => setLoading(false));
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onConnectDatabase();
};
return (
<Tabbed tabName="connect">
<div className={styles.connect_db_content}>
<form onSubmit={onSubmit} className={styles.connect_db_content}>
<h4
className={`${styles.remove_pad_bottom} ${styles.connect_db_header}`}
>
@ -223,7 +228,10 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
title: string;
disableOnEdit: boolean;
}) => (
<label className={styles.connect_db_radio_label}>
<label
key={`label-${radioBtn.title}`}
className={styles.connect_db_radio_label}
>
<input
type="radio"
value={radioBtn.value}
@ -250,7 +258,10 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
label="Database Display Name"
placeholder="database name"
/>
<label className={styles.connect_db_input_label}>
<label
key="Data Source Driver"
className={styles.connect_db_input_label}
>
Data Source Driver
</label>
<select
@ -290,7 +301,7 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
{connectionType === connectionTypes.ENV_VAR ? (
<LabeledInput
label="Environment Variable"
placeholder="DB_URL_FROM_ENV"
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
onChange={e =>
connectDBDispatch({
type: 'UPDATE_DB_URL_ENV_VAR',
@ -448,9 +459,9 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
</div>
<div className={styles.add_button_layout}>
<Button
onClick={onClickConnectDatabase}
size="large"
color="yellow"
type="submit"
style={{
width: '70%',
...(loading && { cursor: 'progress' }),
@ -461,7 +472,7 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
</Button>
</div>
</div>
</div>
</form>
</Tabbed>
);
};

View File

@ -17,7 +17,7 @@ const TabbedDSConnection: React.FC<{ tabName: 'create' | 'connect' }> = ({
url: appPrefix,
},
{
title: 'Manage Databases',
title: 'Data Manager',
url: `${appPrefix}/manage`,
},
{
@ -35,7 +35,7 @@ const TabbedDSConnection: React.FC<{ tabName: 'create' | 'connect' }> = ({
<CommonTabLayout
appPrefix={appPrefix}
currentTab={tabName}
heading="Create database"
heading="Connect Database"
tabsInfo={tabs}
breadCrumbs={breadCrumbs}
baseUrl={`${appPrefix}/manage`}

View File

@ -5,7 +5,7 @@ import styles from './DataSources.scss';
const tabs: Tabs = {
connect: {
display_text: 'Connect existing database',
display_text: 'Connect Existing Database',
},
};
if (Globals.hasuraCloudTenantId && Globals.herokuOAuthClientId) {

View File

@ -29,6 +29,7 @@ const DataSubSidebar = props => {
currentSchema,
enums,
inconsistentObjects,
pathname,
} = props;
const getItems = (schemaInfo = null) => {
@ -146,7 +147,14 @@ const DataSubSidebar = props => {
useEffect(() => {
updateTreeViewItemsWithSchemaInfo();
}, [sources.length, tables, functions, enums, schemaList]);
}, [
sources.length,
tables,
functions,
enums,
schemaList,
inconsistentObjects,
]);
const databasesCount = treeViewItems?.length || 0;
@ -165,6 +173,7 @@ const DataSubSidebar = props => {
onSchemaChange={onSchemaChange}
currentDataSource={currentDataSource}
currentSchema={currentSchema}
pathname={pathname}
/>
</LeftSubSidebar>
);
@ -187,6 +196,7 @@ const mapStateToProps = state => {
currentDataSource: state.tables.currentDataSource,
currentSchema: state.tables.currentSchema,
schemaList: state.tables.schemaList,
pathname: state?.routing?.locationBeforeTransitions?.pathname,
};
};

View File

@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import Helmet from 'react-helmet';
import { connect, ConnectedProps } from 'react-redux';
import Button from '../../../Common/Button/Button';
import styles from '../../../Common/Common.scss';
import styles from './styles.scss';
import { ReduxState } from '../../../../types';
import BreadCrumb from '../../../Common/Layout/BreadCrumb/BreadCrumb';
import { DataSource } from '../../../../metadata/types';
@ -33,10 +33,12 @@ type DatabaseListItemProps = {
// onEdit: (dbName: string) => void;
onReload: (name: string, driver: Driver, cb: () => void) => void;
onRemove: (name: string, driver: Driver, cb: () => void) => void;
pushRoute: (route: string) => void;
};
const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
// onEdit,
pushRoute,
onReload,
onRemove,
dataSource,
@ -46,18 +48,27 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
const [removing, setRemoving] = useState(false);
const [showUrl, setShowUrl] = useState(false);
const viewDB = () => {
if (dataSource?.name) pushRoute(`/data/${dataSource.name}`);
};
const isInconsistentDataSource = isInconsistentSource(
dataSource.name,
inconsistentObjects
);
return (
<div className={styles.db_list_item}>
<div className={styles.db_item_actions}>
{/* Edit shall be cut out until we have a server API
<div
className={`${styles.flex_space_between} ${styles.add_pad_min} ${styles.db_list_item}`}
>
<div className={styles.display_flex}>
<Button
size="xs"
color="white"
style={{ marginRight: '10px' }}
onClick={() => onEdit(dataSource.name)}
onClick={viewDB}
disabled={isInconsistentDataSource}
>
Edit
</Button> */}
View Database
</Button>
<Button
size="xs"
color="white"
@ -84,32 +95,31 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
>
{removing ? 'Removing...' : 'Remove'}
</Button>
</div>
<div className={styles.db_list_content}>
<div className={styles.db_display_data}>
<div className={styles.displayFlexContainer}>
<b>{dataSource.name}</b>&nbsp;
<p>({driverToLabel[dataSource.driver]})</p>
</div>
<p style={{ marginTop: -5 }}>
{getHostFromConnectionString(dataSource)}
</p>
<div
className={`${styles.displayFlexContainer} ${styles.add_pad_left} ${styles.add_pad_top_10}`}
>
<b>{dataSource.name}</b>&nbsp;
<p>({driverToLabel[dataSource.driver]})</p>
</div>
{isInconsistentSource(dataSource.name, inconsistentObjects) && (
<p className={`${styles.add_pad_top_10} ${styles.add_pad_left}`}>
{getHostFromConnectionString(dataSource)}
</p>
{isInconsistentDataSource && (
<ToolTip
id={`inconsistent-source-${dataSource.name}`}
placement="right"
message="Inconsistent Data Source"
>
<i
className="fa fa-exclamation-triangle"
className={`fa fa-exclamation-triangle ${styles.inconsistentSourceIcon}`}
aria-hidden="true"
style={{ padding: 3, color: '#c02020' }}
/>
</ToolTip>
)}
</div>
<span style={{ paddingLeft: 125 }}>
<span
className={`${styles.db_large_string_break_words} ${styles.add_pad_top_10}`}
>
{showUrl ? (
typeof dataSource.url === 'string' ? (
dataSource.url
@ -149,11 +159,20 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
interface ManageDatabaseProps extends InjectedProps {}
let autoRedirectedToConnectPage = false;
const ManageDatabase: React.FC<ManageDatabaseProps> = ({
dataSources,
dispatch,
inconsistentObjects,
location,
}) => {
useEffect(() => {
if (dataSources.length === 0 && !autoRedirectedToConnectPage) {
dispatch(_push('/data/manage/connect'));
autoRedirectedToConnectPage = true;
}
}, [location, dataSources, dispatch]);
const crumbs = [
{
title: 'Data',
@ -192,6 +211,10 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
dispatch(_push('/data/manage/connect'));
};
const pushRoute = (route: string) => {
if (route) dispatch(_push(route));
};
// const onEdit = (dbName: string) => {
// dispatch(_push(`/data/manage/edit/${dbName}`));
// };
@ -206,7 +229,7 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
<h2
className={`${styles.headerText} ${styles.display_inline} ${styles.add_mar_right}`}
>
Manage Databases
Data Manager
</h2>
<Button
color="yellow"
@ -220,17 +243,15 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
</div>
<div className={styles.manage_db_content}>
<hr />
<h3 className={`${styles.heading_text} ${styles.remove_pad_bottom}`}>
Connected Databases
</h3>
<div className={styles.data_list_container}>
<h3 className={styles.heading_text}>Connected Databases</h3>
<div className={styles.flexColumn}>
{dataSources.length ? (
dataSources.map(data => (
<DatabaseListItem
key={data.name}
dataSource={data}
inconsistentObjects={inconsistentObjects}
// onEdit={onEdit}
pushRoute={pushRoute}
onReload={onReload}
onRemove={onRemove}
/>
@ -255,6 +276,7 @@ const mapStateToProps = (state: ReduxState) => {
currentDataSource: state.tables.currentDataSource,
currentSchema: state.tables.currentSchema,
inconsistentObjects: state.metadata.inconsistentObjects,
location: state?.routing?.locationBeforeTransitions,
};
};

View File

@ -44,6 +44,21 @@ import { RightContainer } from '../../../Common/Layout/RightContainer';
import { TrackableFunctionsList } from './FunctionsList';
import { getTrackableFunctions } from './utils';
const FEATURES = {
UNTRACKED_TABLES: 'UNTRACKED_TABLES',
UNTRACKED_RELATIONS: 'UNTRACKED_RELATIONS',
UNTRACKED_FUNCTION: 'UNTRACKED_FUNCTION',
NON_TRACKABLE_FUNCTIONS: 'NON_TRACKABLE_FUNCTIONS',
};
const supportedFeaturesByDriver = {
postgres: Object.values(FEATURES),
mssql: [FEATURES.UNTRACKED_TABLES, FEATURES.UNTRACKED_RELATIONS],
};
const isAllowed = (driver, feature) =>
supportedFeaturesByDriver[driver].includes(feature);
const DeleteSchemaButton = ({ dispatch, migrationMode, currentDataSource }) => {
const successCb = () => {
dispatch(updateCurrentSchema('public', currentDataSource));
@ -540,7 +555,7 @@ class Schema extends Component {
);
};
const getUntrackedFunctionsSection = () => {
const getUntrackedFunctionsSection = isSupported => {
const heading = getSectionHeading(
'Untracked custom functions',
'Custom functions that are not exposed over the GraphQL API',
@ -555,12 +570,18 @@ class Schema extends Component {
testId={'toggle-trackable-functions'}
>
<div className={`${styles.padd_left_remove} col-xs-12`}>
<TrackableFunctionsList
dispatch={dispatch}
funcs={trackableFuncs}
readOnlyMode={readOnlyMode}
source={currentDataSource}
/>
{isSupported ? (
<TrackableFunctionsList
dispatch={dispatch}
funcs={trackableFuncs}
readOnlyMode={readOnlyMode}
source={currentDataSource}
/>
) : (
`Currently unsupported for ${(
currentDriver + ''
).toUpperCase()}`
)}
</div>
<div className={styles.clear_fix} />
</CollapsibleToggle>
@ -646,8 +667,11 @@ class Schema extends Component {
<hr />
{getUntrackedTablesSection()}
{getUntrackedRelationsSection()}
{getUntrackedFunctionsSection()}
{getNonTrackableFunctionsSection()}
{getUntrackedFunctionsSection(
isAllowed(currentDriver, FEATURES.NON_TRACKABLE_FUNCTIONS)
)}
{isAllowed(currentDriver, FEATURES.NON_TRACKABLE_FUNCTIONS) &&
getNonTrackableFunctionsSection()}
<hr />
</div>
</div>

View File

@ -46,3 +46,9 @@
width: 420px;
padding: 8px 0;
}
.inconsistentSourceIcon {
padding: 3px;
color: #c02020;
font-size: 15px;
}

View File

@ -29,17 +29,18 @@ type LeafItemsViewProps = {
item: SourceItem;
currentSource: string;
currentSchema: string;
setActiveTable: (value: string) => void;
isActive: boolean;
pathname: string;
};
const LeafItemsView: React.FC<LeafItemsViewProps> = ({
item,
currentSource,
currentSchema,
setActiveTable,
isActive,
pathname,
}) => {
const [isOpen, setIsOpen] = useState(false);
const isActive = pathname.includes(
`/data/${currentSource}/schema/${currentSchema}/tables/${item.name}`
);
const isView = item.type === 'view';
@ -59,11 +60,9 @@ const LeafItemsView: React.FC<LeafItemsViewProps> = ({
<div
onClick={() => {
setIsOpen(prev => !prev);
setActiveTable(item.name);
}}
onKeyDown={() => {
setIsOpen(prev => !prev);
setActiveTable(item.name);
}}
role="button"
>
@ -126,27 +125,21 @@ type SchemaItemsViewProps = {
currentSource: string;
isActive: boolean;
setActiveSchema: (value: string) => void;
pathname: string;
};
const SchemaItemsView: React.FC<SchemaItemsViewProps> = ({
item,
currentSource,
isActive,
setActiveSchema,
pathname,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [activeTable, setActiveTable] = useState<string | null>(null);
useEffect(() => {
if (!isActive) {
setActiveTable(null);
}
setIsOpen(isActive);
}, [isActive]);
const handleActiveTable = (value: string) => {
setActiveTable(value);
};
return (
<>
<div
@ -178,9 +171,8 @@ const SchemaItemsView: React.FC<SchemaItemsViewProps> = ({
item={child}
currentSource={currentSource}
currentSchema={item.name}
setActiveTable={handleActiveTable}
isActive={activeTable === child.name}
key={key}
pathname={pathname}
/>
</li>
))
@ -196,6 +188,7 @@ type DatabaseItemsViewProps = {
setActiveDataSource: (activeSource: string) => void;
onSchemaChange: (value: string) => void;
currentSchema: string;
pathname: string;
};
const DatabaseItemsView: React.FC<DatabaseItemsViewProps> = ({
item,
@ -203,6 +196,7 @@ const DatabaseItemsView: React.FC<DatabaseItemsViewProps> = ({
setActiveDataSource,
onSchemaChange,
currentSchema,
pathname,
}) => {
const [isOpen, setIsOpen] = useState(false);
@ -246,6 +240,7 @@ const DatabaseItemsView: React.FC<DatabaseItemsViewProps> = ({
isActive={child.name === currentSchema}
setActiveSchema={handleSelectSchema}
key={key}
pathname={pathname}
/>
</li>
))
@ -260,6 +255,7 @@ type TreeViewProps = {
onSchemaChange: (value: string) => void;
currentDataSource: string;
currentSchema: string;
pathname: string;
};
const TreeView: React.FC<TreeViewProps> = ({
items,
@ -267,6 +263,7 @@ const TreeView: React.FC<TreeViewProps> = ({
currentDataSource,
onSchemaChange,
currentSchema,
pathname,
}) => {
const handleSelectDataSource = (dataSource: string) => {
onDatabaseChange(dataSource);
@ -290,6 +287,7 @@ const TreeView: React.FC<TreeViewProps> = ({
isActive={currentDataSource === item.name}
setActiveDataSource={handleSelectDataSource}
currentSchema={currentSchema}
pathname={pathname}
/>
))}
</div>

View File

@ -4,6 +4,7 @@ import { HasuraMetadataV2, HasuraMetadataV3, RestEndpointEntry } from './types';
import {
showSuccessNotification,
showErrorNotification,
showNotification,
} from '../components/Services/Common/Notification';
import {
deleteAllowListQuery,
@ -254,18 +255,35 @@ export const addDataSource = (
return dispatch(requestAction(Endpoints.metadata, options))
.then(() => {
successCb();
if (!skipNotification) {
dispatch(showSuccessNotification('Data source added successfully!'));
}
dispatch(exportMetadata());
dispatch({
type: UPDATE_CURRENT_DATA_SOURCE,
source: data.payload.name,
});
setDriver(data.driver);
dispatch(fetchDataInit(data.payload.name, data.driver));
return getState();
const onButtonClick = () => {
if (data.payload.name) dispatch(_push(`/data/${data.payload.name}`));
};
return dispatch(exportMetadata()).then(() => {
dispatch(fetchDataInit(data.payload.name, data.driver));
if (!skipNotification) {
dispatch(
showNotification(
{
title: 'Database added successfully!',
level: 'success',
autoDismiss: 0,
action: {
label: 'View Database',
callback: onButtonClick,
},
},
'success'
)
);
}
successCb();
return getState();
});
})
.catch(err => {
console.error(err);
@ -366,34 +384,6 @@ export const editDataSource = (
});
};
export const reloadDataSource = (
data: ReloadDataSourceRequest['data']
): Thunk<Promise<void | ReduxState>, MetadataActions> => (
dispatch,
getState
) => {
const { dataHeaders } = getState().tables;
const query = reloadSource(data.name);
const options = {
method: 'POST',
headers: dataHeaders,
body: JSON.stringify(query),
};
return dispatch(requestAction(Endpoints.metadata, options))
.then(() => {
dispatch(showSuccessNotification('Data source reloaded successfully!'));
dispatch(exportMetadata());
return getState();
})
.catch(err => {
console.error(err);
dispatch(showErrorNotification('Reload data source failed', null, err));
});
};
export const replaceMetadata = (
newMetadata: HasuraMetadataV2,
successCb: () => void,
@ -461,6 +451,7 @@ export const resetMetadata = (
requestAction(Endpoints.metadata, options as RequestInit)
).then(
() => {
dispatch({ type: UPDATE_CURRENT_DATA_SOURCE, source: '' });
dispatch(exportMetadata());
if (successCb) {
successCb();
@ -584,7 +575,34 @@ export const loadInconsistentObjects = (
);
};
};
export const reloadDataSource = (
data: ReloadDataSourceRequest['data']
): Thunk<Promise<void | ReduxState>, MetadataActions> => (
dispatch,
getState
) => {
const { dataHeaders } = getState().tables;
const query = reloadSource(data.name);
const options = {
method: 'POST',
headers: dataHeaders,
body: JSON.stringify(query),
};
return dispatch(requestAction(Endpoints.metadata, options))
.then(() => {
dispatch(showSuccessNotification('Data source reloaded successfully!'));
dispatch(exportMetadata());
dispatch(loadInconsistentObjects({ shouldReloadMetadata: false }));
return getState();
})
.catch(err => {
console.error(err);
dispatch(showErrorNotification('Reload data source failed', null, err));
});
};
export const reloadRemoteSchema = (
remoteSchemaName: string,
successCb: () => void,