Skip to content
Snippets Groups Projects
Unverified Commit a3bce7a2 authored by Alexander Lesnenko's avatar Alexander Lesnenko Committed by GitHub
Browse files

do not show restore sample database button to non-admins (#22506)

* do not show restore sample database button to non-admins

* fix db list update after restoring the sample db
parent cc41d39f
No related branches found
No related tags found
No related merge requests found
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import { t } from "ttag";
import cx from "classnames";
import { isSyncCompleted } from "metabase/lib/syncing";
import LoadingSpinner from "metabase/components/LoadingSpinner";
import FormMessage from "metabase/components/form/FormMessage";
import Modal from "metabase/components/Modal";
import SyncingModal from "metabase/containers/SyncingModal";
import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
import {
TableCellContent,
TableCellSpinner,
} from "../../containers/DatabaseListApp.styled";
const query = {
...PLUGIN_FEATURE_LEVEL_PERMISSIONS.databaseDetailsQueryProps,
};
export default class DatabaseList extends Component {
constructor(props) {
super(props);
props.databases.map(database => {
this["deleteDatabaseModal_" + database.id] = React.createRef();
});
this.state = {
isSyncingModalOpened: (props.created && props.showSyncingModal) || false,
};
}
componentDidMount() {
if (this.state.isSyncingModalOpened) {
this.props.closeSyncingModal();
}
}
onSyncingModalClose = () => {
this.setState({ isSyncingModalOpened: false });
};
static propTypes = {
databases: PropTypes.array,
hasSampleDatabase: PropTypes.bool,
engines: PropTypes.object,
deletes: PropTypes.array,
deletionError: PropTypes.object,
created: PropTypes.string,
showSyncingModal: PropTypes.bool,
closeSyncingModal: PropTypes.func,
isAdmin: PropTypes.bool,
};
render() {
const {
databases,
hasSampleDatabase,
isAddingSampleDatabase,
addSampleDatabaseError,
engines,
deletionError,
isAdmin,
} = this.props;
const { isSyncingModalOpened } = this.state;
const error = deletionError || addSampleDatabaseError;
return (
<div className="wrapper">
<section className="PageHeader px2 clearfix">
{isAdmin && (
<Link
to="/admin/databases/create"
className="Button Button--primary float-right"
>{t`Add database`}</Link>
)}
<h2 className="PageTitle">{t`Databases`}</h2>
</section>
{error && (
<section>
<FormMessage formError={error} />
</section>
)}
<section>
<table className="ContentTable">
<thead>
<tr>
<th>{t`Name`}</th>
<th>{t`Engine`}</th>
</tr>
</thead>
<tbody>
{databases ? (
[
databases.map(database => {
const isDeleting =
this.props.deletes.indexOf(database.id) !== -1;
return (
<tr
key={database.id}
className={cx({ disabled: isDeleting })}
>
<td>
<TableCellContent>
{!isSyncCompleted(database) && (
<TableCellSpinner size={16} borderWidth={2} />
)}
<Link
to={"/admin/databases/" + database.id}
className="text-bold link"
>
{database.name}
</Link>
</TableCellContent>
</td>
<td>
{engines && engines[database.engine]
? engines[database.engine]["driver-name"]
: database.engine}
</td>
</tr>
);
}),
]
) : (
<tr>
<td colSpan={4}>
<LoadingSpinner />
<h3>{t`Loading ...`}</h3>
</td>
</tr>
)}
</tbody>
</table>
{!hasSampleDatabase && isAdmin ? (
<div className="pt4">
<span
className={cx("p2 text-italic", {
"border-top": databases && databases.length > 0,
})}
>
{isAddingSampleDatabase ? (
<span className="text-light no-decoration">
{t`Restoring the sample database...`}
</span>
) : (
<a
className="text-light text-brand-hover no-decoration"
onClick={() => this.props.addSampleDatabase(query)}
>
{t`Bring the sample database back`}
</a>
)}
</span>
</div>
) : null}
</section>
<Modal
small
isOpen={isSyncingModalOpened}
onClose={this.onSyncingModalClose}
>
<SyncingModal onClose={this.onSyncingModalClose} />
</Modal>
</div>
);
}
}
import React from "react";
import { render, screen } from "__support__/ui";
import DatabaseList from "./DatabaseList";
import { createMockDatabase } from "metabase-types/api/mocks";
const CREATE_SAMPLE_DATABASE_BUTTON_LABEL = "Bring the sample database back";
async function setup({ hasSampleDatabase, isAdmin } = {}) {
const databases = [createMockDatabase()];
render(
<DatabaseList
databases={databases}
hasSampleDatabase={hasSampleDatabase}
isAdmin={isAdmin}
deletes={[]}
/>,
);
}
describe("DatabaseListApp", () => {
it("shows the restore sample database button to admins when there is no sample database", async () => {
await setup({ hasSampleDatabase: false, isAdmin: true });
expect(
screen.queryByText(CREATE_SAMPLE_DATABASE_BUTTON_LABEL),
).toBeInTheDocument();
});
it("does not show the restore sample database button to admins when the sample database exists", async () => {
await setup({ hasSampleDatabase: true, isAdmin: true });
expect(
screen.queryByText(CREATE_SAMPLE_DATABASE_BUTTON_LABEL),
).not.toBeInTheDocument();
});
it("does not show restore sample database button to non-admins", async () => {
await setup({ hasSampleDatabase: false, isAdmin: false });
expect(
screen.queryByText(CREATE_SAMPLE_DATABASE_BUTTON_LABEL),
).not.toBeInTheDocument();
});
});
export { default } from "./DatabaseList";
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Link } from "react-router";
import { t } from "ttag";
import _ from "underscore";
import cx from "classnames";
import MetabaseSettings from "metabase/lib/settings";
import { isSyncCompleted, isSyncInProgress } from "metabase/lib/syncing";
import { isSyncInProgress } from "metabase/lib/syncing";
import LoadingSpinner from "metabase/components/LoadingSpinner";
import LoadingAndGenericErrorWrapper from "metabase/components/LoadingAndGenericErrorWrapper";
import FormMessage from "metabase/components/form/FormMessage";
import Modal from "metabase/components/Modal";
import SyncingModal from "metabase/containers/SyncingModal";
import { getUserIsAdmin } from "metabase/selectors/user";
import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
import { TableCellContent, TableCellSpinner } from "./DatabaseListApp.styled";
import DatabaseList from "../components/DatabaseList";
import Database from "metabase/entities/databases";
......@@ -33,16 +21,23 @@ import {
addSampleDatabase,
closeSyncingModal,
} from "../database";
import _ from "underscore";
const RELOAD_INTERVAL = 2000;
const getReloadInterval = (state, props, databases = []) => {
const getReloadInterval = (_state, _props, databases = []) => {
return databases.some(d => isSyncInProgress(d)) ? RELOAD_INTERVAL : 0;
};
const query = {
...PLUGIN_FEATURE_LEVEL_PERMISSIONS.databaseDetailsQueryProps,
};
const mapStateToProps = (state, props) => ({
isAdmin: getUserIsAdmin(state),
hasSampleDatabase: Database.selectors.getHasSampleDatabase(state),
hasSampleDatabase: Database.selectors.getHasSampleDatabase(state, {
entityQuery: query,
}),
isAddingSampleDatabase: getIsAddingSampleDatabase(state),
addSampleDatabaseError: getAddSampleDatabaseError(state),
......@@ -54,10 +49,6 @@ const mapStateToProps = (state, props) => ({
deletionError: getDeletionError(state),
});
const query = {
...PLUGIN_FEATURE_LEVEL_PERMISSIONS.databaseDetailsQueryProps,
};
const mapDispatchToProps = {
// NOTE: still uses deleteDatabase from metabaseadmin/databases/databases.js
// rather than metabase/entities/databases since it updates deletes/deletionError
......@@ -66,156 +57,6 @@ const mapDispatchToProps = {
closeSyncingModal,
};
class DatabaseList extends Component {
constructor(props) {
super(props);
props.databases.map(database => {
this["deleteDatabaseModal_" + database.id] = React.createRef();
});
this.state = {
isSyncingModalOpened: (props.created && props.showSyncingModal) || false,
};
}
componentDidMount() {
if (this.state.isSyncingModalOpened) {
this.props.closeSyncingModal();
}
}
onSyncingModalClose = () => {
this.setState({ isSyncingModalOpened: false });
};
static propTypes = {
databases: PropTypes.array,
hasSampleDatabase: PropTypes.bool,
engines: PropTypes.object,
deletes: PropTypes.array,
deletionError: PropTypes.object,
created: PropTypes.string,
showSyncingModal: PropTypes.bool,
closeSyncingModal: PropTypes.func,
};
render() {
const {
databases,
hasSampleDatabase,
isAddingSampleDatabase,
addSampleDatabaseError,
engines,
deletionError,
isAdmin,
} = this.props;
const { isSyncingModalOpened } = this.state;
const error = deletionError || addSampleDatabaseError;
return (
<div className="wrapper">
<section className="PageHeader px2 clearfix">
{isAdmin && (
<Link
to="/admin/databases/create"
className="Button Button--primary float-right"
>{t`Add database`}</Link>
)}
<h2 className="PageTitle">{t`Databases`}</h2>
</section>
{error && (
<section>
<FormMessage formError={error} />
</section>
)}
<section>
<table className="ContentTable">
<thead>
<tr>
<th>{t`Name`}</th>
<th>{t`Engine`}</th>
</tr>
</thead>
<tbody>
{databases ? (
[
databases.map(database => {
const isDeleting =
this.props.deletes.indexOf(database.id) !== -1;
return (
<tr
key={database.id}
className={cx({ disabled: isDeleting })}
>
<td>
<TableCellContent>
{!isSyncCompleted(database) && (
<TableCellSpinner size={16} borderWidth={2} />
)}
<Link
to={"/admin/databases/" + database.id}
className="text-bold link"
>
{database.name}
</Link>
</TableCellContent>
</td>
<td>
{engines && engines[database.engine]
? engines[database.engine]["driver-name"]
: database.engine}
</td>
</tr>
);
}),
]
) : (
<tr>
<td colSpan={4}>
<LoadingSpinner />
<h3>{t`Loading ...`}</h3>
</td>
</tr>
)}
</tbody>
</table>
{!hasSampleDatabase ? (
<div className="pt4">
<span
className={cx("p2 text-italic", {
"border-top": databases && databases.length > 0,
})}
>
{isAddingSampleDatabase ? (
<span className="text-light no-decoration">
{t`Restoring the sample database...`}
</span>
) : (
<a
className="text-light text-brand-hover no-decoration"
onClick={() => this.props.addSampleDatabase(query)}
>
{t`Bring the sample database back`}
</a>
)}
</span>
</div>
) : null}
</section>
<Modal
small
isOpen={isSyncingModalOpened}
onClose={this.onSyncingModalClose}
>
<SyncingModal onClose={this.onSyncingModalClose} />
</Modal>
</div>
);
}
}
export default _.compose(
Database.loadList({
reloadInterval: getReloadInterval,
......
......@@ -75,8 +75,8 @@ const Databases = createEntity({
selectors: {
getObject: (state, { entityId }) => getMetadata(state).database(entityId),
getHasSampleDatabase: state =>
_.any(Databases.selectors.getList(state), db => db.is_sample),
getHasSampleDatabase: (state, props) =>
_.any(Databases.selectors.getList(state, props), db => db.is_sample),
getIdfields: createSelector(
// we wrap getFields to handle a circular dep issue
[state => getFields(state), (state, props) => props.databaseId],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment