diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
index 119f15fddebd0..77f6109db52a9 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
@@ -76,6 +76,18 @@ import {
} from './styles';
import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader';
+const engineSpecificAlertMapping = {
+ gsheets: {
+ message: 'Why do I need to create a database?',
+ description:
+ 'To begin using your Google Sheets, you need to create a database first. ' +
+ 'Databases are used as a way to identify ' +
+ 'your data so that it can be queried and visualized. This ' +
+ 'database will hold all of your individual Google Sheets ' +
+ 'you choose to connect here.',
+ },
+};
+
const errorAlertMapping = {
CONNECTION_MISSING_PARAMETERS_ERROR: {
message: 'Missing Required Fields',
@@ -134,6 +146,7 @@ enum ActionType {
extraEditorChange,
addTableCatalogSheet,
removeTableCatalogSheet,
+ queryChange,
}
interface DBReducerPayloadType {
@@ -151,6 +164,7 @@ type DBReducerActionType =
| ActionType.extraEditorChange
| ActionType.extraInputChange
| ActionType.textChange
+ | ActionType.queryChange
| ActionType.inputChange
| ActionType.editorChange
| ActionType.parametersChange;
@@ -193,7 +207,8 @@ function dbReducer(
const trimmedState = {
...(state || {}),
};
- let query = '';
+ let query = {};
+ let query_input = '';
let deserializeExtraJSON = {};
let extra_json: DatabaseObject['extra_json'];
@@ -306,6 +321,15 @@ function dbReducer(
...trimmedState,
[action.payload.name]: action.payload.json,
};
+ case ActionType.queryChange:
+ return {
+ ...trimmedState,
+ parameters: {
+ ...trimmedState.parameters,
+ query: Object.fromEntries(new URLSearchParams(action.payload.value)),
+ },
+ query_input: action.payload.value,
+ };
case ActionType.textChange:
return {
...trimmedState,
@@ -327,6 +351,12 @@ function dbReducer(
};
}
+ // convert query to a string and store in query_input
+ query = action.payload?.parameters?.query || {};
+ query_input = Object.entries(query)
+ .map(([key, value]) => `${key}=${value}`)
+ .join('&');
+
if (
action.payload.backend === 'bigquery' &&
action.payload.configuration_method ===
@@ -338,11 +368,12 @@ function dbReducer(
configuration_method: action.payload.configuration_method,
extra_json: deserializeExtraJSON,
parameters: {
- query,
credentials_info: JSON.stringify(
action.payload?.parameters?.credentials_info || '',
),
+ query,
},
+ query_input,
};
}
@@ -364,37 +395,18 @@ function dbReducer(
name: e,
value: engineParamsCatalog[e],
})),
+ query_input,
} as DatabaseObject;
}
- if (action.payload?.parameters?.query) {
- // convert query into URI params string
- query = new URLSearchParams(
- action.payload.parameters.query as string,
- ).toString();
-
- return {
- ...action.payload,
- encrypted_extra: action.payload.encrypted_extra || '',
- engine: action.payload.backend || trimmedState.engine,
- configuration_method: action.payload.configuration_method,
- extra_json: deserializeExtraJSON,
- parameters: {
- ...action.payload.parameters,
- query,
- },
- };
- }
-
return {
...action.payload,
encrypted_extra: action.payload.encrypted_extra || '',
engine: action.payload.backend || trimmedState.engine,
configuration_method: action.payload.configuration_method,
extra_json: deserializeExtraJSON,
- parameters: {
- ...action.payload.parameters,
- },
+ parameters: action.payload.parameters,
+ query_input,
};
case ActionType.dbSelected:
@@ -454,10 +466,11 @@ const DatabaseModal: FunctionComponent
= ({
const sslForced = isFeatureEnabled(
FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL,
);
+ const hasAlert =
+ connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
const useSqlAlchemyForm =
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
const useTabLayout = isEditMode || useSqlAlchemyForm;
-
// Database fetch logic
const {
state: { loading: dbLoading, resource: dbFetched, error: dbErrors },
@@ -471,9 +484,9 @@ const DatabaseModal: FunctionComponent = ({
addDangerToast,
);
const isDynamic = (engine: string | undefined) =>
- availableDbs?.databases.filter(
+ availableDbs?.databases?.find(
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
- )[0].parameters !== undefined;
+ )?.parameters !== undefined;
const showDBError = validationErrors || dbErrors;
const isEmpty = (data?: Object | null) =>
data && Object.keys(data).length === 0;
@@ -527,21 +540,6 @@ const DatabaseModal: FunctionComponent = ({
return;
}
- if (dbToUpdate?.parameters?.query) {
- // convert query params into dictionary
- dbToUpdate.parameters.query = JSON.parse(
- `{"${decodeURI((dbToUpdate?.parameters?.query as string) || '')
- .replace(/"/g, '\\"')
- .replace(/&/g, '","')
- .replace(/=/g, '":"')}"}`,
- );
- } else if (
- dbToUpdate?.parameters?.query === '' &&
- 'query' in dbModel.parameters.properties
- ) {
- dbToUpdate.parameters.query = {};
- }
-
const engine = dbToUpdate.backend || dbToUpdate.engine;
if (engine === 'bigquery' && dbToUpdate.parameters?.credentials_info) {
// wrap encrypted_extra in credentials_info only for BigQuery
@@ -642,21 +640,34 @@ const DatabaseModal: FunctionComponent = ({
};
const setDatabaseModel = (database_name: string) => {
- const selectedDbModel = availableDbs?.databases.filter(
- (db: DatabaseObject) => db.name === database_name,
- )[0];
- const { engine, parameters } = selectedDbModel;
- const isDynamic = parameters !== undefined;
- setDB({
- type: ActionType.dbSelected,
- payload: {
- database_name,
- configuration_method: isDynamic
- ? CONFIGURATION_METHOD.DYNAMIC_FORM
- : CONFIGURATION_METHOD.SQLALCHEMY_URI,
- engine,
- },
- });
+ if (database_name === 'Other') {
+ // Allow users to connect to DB via legacy SQLA form
+ setDB({
+ type: ActionType.dbSelected,
+ payload: {
+ database_name,
+ configuration_method: CONFIGURATION_METHOD.SQLALCHEMY_URI,
+ engine: undefined,
+ },
+ });
+ } else {
+ const selectedDbModel = availableDbs?.databases.filter(
+ (db: DatabaseObject) => db.name === database_name,
+ )[0];
+ const { engine, parameters } = selectedDbModel;
+ const isDynamic = parameters !== undefined;
+ setDB({
+ type: ActionType.dbSelected,
+ payload: {
+ database_name,
+ configuration_method: isDynamic
+ ? CONFIGURATION_METHOD.DYNAMIC_FORM
+ : CONFIGURATION_METHOD.SQLALCHEMY_URI,
+ engine,
+ },
+ });
+ }
+
setDB({ type: ActionType.addTableCatalogSheet });
};
@@ -680,6 +691,10 @@ const DatabaseModal: FunctionComponent = ({
{database.name}
))}
+ {/* Allow users to connect to DB via legacy SQLA form */}
+
+ Other
+
= ({
setTabKey(key);
};
+ const renderStepTwoAlert = () => {
+ const { hostname } = window.location;
+ let ipAlert = connectionAlert?.REGIONAL_IPS?.default || '';
+ const regionalIPs = connectionAlert?.REGIONAL_IPS || {};
+ Object.entries(regionalIPs).forEach(([regex, ipRange]) => {
+ if (regex.match(hostname)) {
+ ipAlert = ipRange;
+ }
+ });
+ return (
+ db?.engine && (
+
+ antDAlertStyles(theme)}
+ type="info"
+ showIcon
+ message={
+ engineSpecificAlertMapping[db.engine]?.message ||
+ connectionAlert?.DEFAULT?.message
+ }
+ description={
+ engineSpecificAlertMapping[db.engine]?.description ||
+ connectionAlert?.DEFAULT?.description + ipAlert
+ }
+ />
+
+ )
+ );
+ };
+
const errorAlert = () => {
if (
isEmpty(dbErrors) ||
@@ -929,6 +975,12 @@ const DatabaseModal: FunctionComponent = ({
value: target.value,
})
}
+ onQueryChange={({ target }: { target: HTMLInputElement }) =>
+ onChange(ActionType.queryChange, {
+ name: target.name,
+ value: target.value,
+ })
+ }
onAddTableCatalog={() =>
setDB({ type: ActionType.addTableCatalogSheet })
}
@@ -1050,6 +1102,12 @@ const DatabaseModal: FunctionComponent = ({
value: target.value,
})
}
+ onQueryChange={({ target }: { target: HTMLInputElement }) =>
+ onChange(ActionType.queryChange, {
+ name: target.name,
+ value: target.value,
+ })
+ }
onAddTableCatalog={() =>
setDB({ type: ActionType.addTableCatalogSheet })
}
@@ -1188,18 +1246,7 @@ const DatabaseModal: FunctionComponent = ({
dbName={dbName}
dbModel={dbModel}
/>
- {connectionAlert && (
-
- antDAlertStyles(theme)}
- type="info"
- showIcon
- message={t('IP Allowlist')}
- description={connectionAlert.ALLOWED_IPS}
- />
-
- )}
+ {hasAlert && renderStepTwoAlert()}
= ({
onAddTableCatalog={() => {
setDB({ type: ActionType.addTableCatalogSheet });
}}
+ onQueryChange={({ target }: { target: HTMLInputElement }) =>
+ onChange(ActionType.queryChange, {
+ name: target.name,
+ value: target.value,
+ })
+ }
onRemoveTableCatalog={(idx: number) => {
setDB({
type: ActionType.removeTableCatalogSheet,
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/styles.ts b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/styles.ts
index 9a4a2a1237bb3..364ee971e12dc 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/styles.ts
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/styles.ts
@@ -552,13 +552,14 @@ export const StyledCatalogTable = styled.div`
}
.catalog-label {
- margin: 0 0 8px;
+ margin: 0 0 7px;
}
.catalog-name {
display: flex;
.catalog-name-input {
width: 95%;
+ margin-bottom: 0px;
}
}
@@ -570,7 +571,7 @@ export const StyledCatalogTable = styled.div`
.catalog-delete {
align-self: center;
background: ${({ theme }) => theme.colors.grayscale.light4};
- margin: 5px;
+ margin: 5px 5px 8px 5px;
}
.catalog-add-btn {
diff --git a/superset-frontend/src/views/CRUD/data/database/types.ts b/superset-frontend/src/views/CRUD/data/database/types.ts
index f6341d34ee857..d473ba50aac7c 100644
--- a/superset-frontend/src/views/CRUD/data/database/types.ts
+++ b/superset-frontend/src/views/CRUD/data/database/types.ts
@@ -45,15 +45,12 @@ export type DatabaseObject = {
password?: string;
encryption?: boolean;
credentials_info?: string;
- query?: string | object;
- catalog?: {};
+ query?: Record;
+ catalog?: Record;
};
configuration_method: CONFIGURATION_METHOD;
engine?: string;
- // Gsheets temporary storage
- catalog?: Array;
-
// Performance
cache_timeout?: string;
allow_run_async?: boolean;
@@ -85,11 +82,14 @@ export type DatabaseObject = {
allows_virtual_table_explore?: boolean; // in SQL Lab
schemas_allowed_for_csv_upload?: string[]; // in Security
cancel_query_on_windows_unload?: boolean; // in Performance
- version?: string;
- // todo: ask beto where this should live
- cost_query_enabled?: boolean; // in SQL Lab
+ version?: string;
+ cost_estimate_enabled?: boolean; // in SQL Lab
};
+
+ // Temporary storage
+ catalog?: Array;
+ query_input?: string;
extra?: string;
};
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index c1ee1f52c69d1..808d124247f1a 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -678,21 +678,48 @@ export function useDatabaseValidation() {
invalid?: string[];
missing?: string[];
name: string;
+ catalog: {
+ name: string;
+ url: string;
+ idx: number;
+ };
};
message: string;
},
) => {
- // if extra.invalid doesn't exist then the
- // error can't be mapped to a parameter
- // so leave it alone
- if (extra.invalid) {
- if (extra.invalid[0] === 'catalog') {
+ if (extra.catalog) {
+ if (extra.catalog.name) {
+ return {
+ ...obj,
+ error_type,
+ [extra.catalog.idx]: {
+ name: message,
+ },
+ };
+ }
+ if (extra.catalog.url) {
return {
...obj,
- [extra.name]: message,
error_type,
+ [extra.catalog.idx]: {
+ url: message,
+ },
};
}
+
+ return {
+ ...obj,
+ error_type,
+ [extra.catalog.idx]: {
+ name: message,
+ url: message,
+ },
+ };
+ }
+ // if extra.invalid doesn't exist then the
+ // error can't be mapped to a parameter
+ // so leave it alone
+ if (extra.invalid) {
return {
...obj,
[extra.invalid[0]]: message,
diff --git a/superset-frontend/src/views/CRUD/storageKeys.ts b/superset-frontend/src/views/CRUD/storageKeys.ts
index cd8f476405733..5002420aa777a 100644
--- a/superset-frontend/src/views/CRUD/storageKeys.ts
+++ b/superset-frontend/src/views/CRUD/storageKeys.ts
@@ -17,7 +17,8 @@
* under the License.
*/
-// storage keys for welcome page sticky tabs..
+// storage keys for welcome page sticky tabs and tables
export const HOMEPAGE_CHART_FILTER = 'homepage_chart_filter';
export const HOMEPAGE_ACTIVITY_FILTER = 'homepage_activity_filter';
export const HOMEPAGE_DASHBOARD_FILTER = 'homepage_dashboard_filter';
+export const HOMEPAGE_COLLAPSE_STATE = 'homepage_collapse_state';
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index c7e47ddeca9a0..d08ca46a692ea 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -32,7 +32,7 @@ export enum TableTabTypes {
export type Filters = {
col: string;
opr: string;
- value: string;
+ value: string | number;
};
export interface DashboardTableProps {
diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx
index 2b0f80c76b253..08535e1b91b18 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -132,10 +132,19 @@ export const getRecentAcitivtyObjs = (
) =>
SupersetClient.get({ endpoint: recent }).then(recentsRes => {
const res: any = {};
+ const filters = [
+ {
+ col: 'created_by',
+ opr: 'rel_o_m',
+ value: 0,
+ },
+ ];
const newBatch = [
- SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
SupersetClient.get({
- endpoint: `/api/v1/dashboard/?q=${getParams()}`,
+ endpoint: `/api/v1/chart/?q=${getParams(filters)}`,
+ }),
+ SupersetClient.get({
+ endpoint: `/api/v1/dashboard/?q=${getParams(filters)}`,
}),
];
return Promise.all(newBatch)
@@ -269,15 +278,12 @@ export function shortenSQL(sql: string, maxLines: number) {
return lines.join('\n');
}
+// loading card count for homepage
+export const loadingCardCount = 5;
+
const breakpoints = [576, 768, 992, 1200];
export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`);
-export const CardStylesOverrides = styled.div`
- .ant-card-cover > div {
- height: 264px;
- }
-`;
-
export const CardContainer = styled.div<{
showThumbnails?: boolean | undefined;
}>`
@@ -286,7 +292,7 @@ export const CardContainer = styled.div<{
display: grid;
grid-gap: ${theme.gridUnit * 12}px ${theme.gridUnit * 4}px;
grid-template-columns: repeat(auto-fit, 300px);
- max-height: ${showThumbnails ? '314' : '140'}px;
+ max-height: ${showThumbnails ? '314' : '148'}px;
margin-top: ${theme.gridUnit * -6}px;
padding: ${
showThumbnails
diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
index c7fa9679bd876..930cfca8220fb 100644
--- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -21,9 +21,9 @@ import moment from 'moment';
import { styled, t } from '@superset-ui/core';
import { setInLocalStorage } from 'src/utils/localStorageHelpers';
-import Loading from 'src/components/Loading';
import ListViewCard from 'src/components/ListViewCard';
import SubMenu from 'src/components/Menu/SubMenu';
+import { LoadingCards, ActivityData } from 'src/views/CRUD/welcome/Welcome';
import {
CardStyles,
getEditedObjects,
@@ -34,7 +34,7 @@ import { Chart } from 'src/types/Chart';
import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types';
import Icons from 'src/components/Icons';
-import { ActivityData } from './Welcome';
+
import EmptyState from './EmptyState';
/**
@@ -230,7 +230,7 @@ export default function ActivityTable({
const doneFetching = loadedCount < 3;
if ((loadingState && !editedObjs) || doneFetching) {
- return ;
+ return ;
}
return (
diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
index 9c78c81d97fac..d487e8a246c8b 100644
--- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
@@ -35,6 +35,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal';
import { User } from 'src/types/bootstrapTypes';
import { CardContainer, PAGE_SIZE } from 'src/views/CRUD/utils';
import { HOMEPAGE_CHART_FILTER } from 'src/views/CRUD/storageKeys';
+import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import ChartCard from 'src/views/CRUD/chart/ChartCard';
import Chart from 'src/types/Chart';
import handleResourceExport from 'src/utils/export';
@@ -131,6 +132,12 @@ function ChartTable({
operator: 'chart_is_favorite',
value: true,
});
+ } else if (filterName === 'Examples') {
+ filters.push({
+ id: 'created_by',
+ operator: 'rel_o_m',
+ value: 0,
+ });
}
return filters;
};
@@ -177,7 +184,7 @@ function ChartTable({
});
}
- if (loading) return ;
+ if (loading) return ;
return (
{sliceCurrentlyEditing && (
diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
index caba49ca47070..3e04f930c038a 100644
--- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
@@ -31,6 +31,7 @@ import {
setInLocalStorage,
getFromLocalStorage,
} from 'src/utils/localStorageHelpers';
+import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import {
createErrorHandler,
CardContainer,
@@ -142,6 +143,12 @@ function DashboardTable({
operator: 'dashboard_is_favorite',
value: true,
});
+ } else if (filterName === 'Examples') {
+ filters.push({
+ id: 'created_by',
+ operator: 'rel_o_m',
+ value: 0,
+ });
}
return filters;
};
@@ -189,7 +196,7 @@ function DashboardTable({
filters: getFilters(filter),
});
- if (loading) return ;
+ if (loading) return ;
return (
<>
);
- if (loading) return ;
+ if (loading) return ;
return (
<>
{queryDeleteModal && (
diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
index 540b63576dd67..63d7776178b3b 100644
--- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
@@ -25,15 +25,20 @@ import {
getFromLocalStorage,
setInLocalStorage,
} from 'src/utils/localStorageHelpers';
+import ListViewCard from 'src/components/ListViewCard';
import withToasts from 'src/messageToasts/enhancers/withToasts';
-import Loading from 'src/components/Loading';
import {
createErrorHandler,
getRecentAcitivtyObjs,
mq,
+ CardContainer,
getUserOwnedObjects,
+ loadingCardCount,
} from 'src/views/CRUD/utils';
-import { HOMEPAGE_ACTIVITY_FILTER } from 'src/views/CRUD/storageKeys';
+import {
+ HOMEPAGE_ACTIVITY_FILTER,
+ HOMEPAGE_COLLAPSE_STATE,
+} from 'src/views/CRUD/storageKeys';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { Switch } from 'src/common/components';
@@ -54,6 +59,10 @@ export interface ActivityData {
Examples?: Array