Skip to content
Snippets Groups Projects
Commit 717c999c authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'master' of github.com:metabase/metabase into root-collection-permissions

parents 3e650433 70c17317
No related branches found
No related tags found
No related merge requests found
Showing
with 140 additions and 174 deletions
......@@ -17,13 +17,13 @@ export default class CreatedDatabaseModal extends Component {
const { onClose, onDone, databaseId } = this.props;
return (
<ModalContent title={t`Your database has been added!`} onClose={onClose}>
<div className="Form-inputs mb4">
<div className="mb4">
<p>
{t`We took a look at your data, and we have some automated explorations that we can show you!`}
</p>
</div>
<div className="Form-actions flex layout-centered">
<div className="flex layout-centered">
<a className="link" onClick={onDone}>{t`I'm good thanks`}</a>
<Link
to={`/explore/${databaseId}`}
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import { t } from "c-3po";
import Button from "metabase/components/Button";
import ModalContent from "metabase/components/ModalContent.jsx";
import { t } from "c-3po";
import cx from "classnames";
export default class DeleteDatabaseModal extends Component {
constructor(props, context) {
......@@ -53,7 +53,7 @@ export default class DeleteDatabaseModal extends Component {
title={t`Delete this database?`}
onClose={this.props.onClose}
>
<div className="Form-inputs mb4">
<div className="mb4">
{database.is_sample && (
<p className="text-paragraph">{t`<strong>Just a heads up:</strong> without the Sample Dataset, the Query Builder tutorial won't work. You can always restore the Sample Dataset, but any questions you've saved using this data will be lost.`}</p>
)}
......@@ -73,17 +73,14 @@ export default class DeleteDatabaseModal extends Component {
/>
</div>
<div className="Form-actions ml-auto">
<button
className="Button"
onClick={this.props.onClose}
>{t`Cancel`}</button>
<button
className={cx("Button Button--danger ml2", {
disabled: !confirmed,
})}
<div className="ml-auto">
<Button onClick={this.props.onClose}>{t`Cancel`}</Button>
<Button
ml={2}
danger
disabled={!confirmed}
onClick={() => this.deleteDatabase()}
>{t`Delete`}</button>
>{t`Delete`}</Button>
{formError}
</div>
</ModalContent>
......
......@@ -13,37 +13,41 @@ export default class MetadataSchema extends Component {
return false;
}
const tdClassName = "py2 px1 border-bottom";
let fields = tableMetadata.fields.map(field => {
return (
<li key={field.id} className="px1 py2 flex border-bottom">
<div className="flex-full flex flex-column mr1">
<tr key={field.id}>
<td className={tdClassName}>
<span className="TableEditor-field-name text-bold">
{field.name}
</span>
</div>
<div className="flex-half">
</td>
<td className={tdClassName}>
<span className="text-bold">{field.base_type}</span>
</div>
<div className="flex-half" />
</li>
</td>
<td className={tdClassName} />
</tr>
);
});
return (
<div className="MetadataTable px2 flex-full">
<div className="MetadataTable px2 full">
<div className="flex flex-column px1">
<div className="TableEditor-table-name text-bold">
{tableMetadata.name}
</div>
</div>
<div className="mt2 ">
<div className="text-uppercase text-grey-3 py1 flex">
<div className="flex-full px1">{t`Column`}</div>
<div className="flex-half px1">{t`Data Type`}</div>
<div className="flex-half px1">{t`Additional Info`}</div>
</div>
<ol className="border-top border-bottom">{fields}</ol>
</div>
<table className="mt2 full">
<thead className="text-uppercase text-grey-3 py1">
<tr>
<th className={tdClassName}>{t`Column`}</th>
<th className={tdClassName}>{t`Data Type`}</th>
<th className={tdClassName}>{t`Additional Info`}</th>
</tr>
</thead>
<tbody>{fields}</tbody>
</table>
</div>
);
}
......
......@@ -321,17 +321,17 @@ class CollectionLanding extends React.Component {
{
title: t`New dashboard`,
icon: "dashboard",
link: `/questions/archive/`,
link: Urls.newDashboard(collectionId),
},
{
title: t`New pulse`,
icon: "pulse",
link: `/dashboards/archive`,
link: Urls.newPulse(collectionId),
},
{
title: t`New collection`,
icon: "all",
link: `/dashboards/archive`,
link: Urls.newCollection(collectionId),
},
]}
triggerIcon="add"
......
......@@ -2,6 +2,9 @@ import React from "react";
import ModalContent from "metabase/components/ModalContent.jsx";
import { t } from "c-3po";
import Button from "metabase/components/Button";
const nop = () => {};
const ConfirmContent = ({
......@@ -16,36 +19,35 @@ const ConfirmContent = ({
}) => (
<ModalContent
title={title}
formModal
onClose={() => {
onCancel();
onClose();
}}
>
<div className="mx4">{content}</div>
<div>{content}</div>
<div className="Form-inputs mb4">
<p>{message}</p>
</div>
<p className="mb4">{message}</p>
<div className="Form-actions ml-auto">
<button
className="Button"
<div className="ml-auto mb4">
<Button
onClick={() => {
onCancel();
onClose();
}}
>
{cancelButtonText}
</button>
<button
className="Button Button--danger ml2"
</Button>
<Button
danger
ml={2}
onClick={() => {
onAction();
onClose();
}}
>
{confirmButtonText}
</button>
</Button>
</div>
</ModalContent>
);
......
......@@ -2,6 +2,9 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import { Box } from "rebass";
import { t } from "c-3po";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import FormField from "metabase/components/form/FormField.jsx";
import ModalContent from "metabase/components/ModalContent.jsx";
......@@ -10,6 +13,14 @@ import Select, { Option } from "metabase/components/Select.jsx";
import CollectionListLoader from "metabase/containers/CollectionListLoader";
import { createDashboard } from "metabase/dashboards/dashboards";
const mapDispatchToProps = {
createDashboard,
};
@connect(null, mapDispatchToProps)
@withRouter
export default class CreateDashboardModal extends Component {
constructor(props, context) {
super(props, context);
......@@ -17,11 +28,14 @@ export default class CreateDashboardModal extends Component {
this.setDescription = this.setDescription.bind(this);
this.setName = this.setName.bind(this);
console.log(props.params);
this.state = {
name: null,
description: null,
errors: null,
collection_id: null,
// collectionId in the url starts off as a string, but the select will
// compare it to the integer ID on colleciton objects
collection_id: parseInt(props.params.collectionId),
};
}
......@@ -77,6 +91,10 @@ export default class CreateDashboardModal extends Component {
title={t`Create dashboard`}
footer={[
formError,
<Button
mr={1}
onClick={() => this.props.onClose()}
>{t`Cancel`}</Button>,
<Button
primary={formReady}
disabled={!formReady}
......@@ -86,7 +104,7 @@ export default class CreateDashboardModal extends Component {
onClose={this.props.onClose}
>
<form className="Modal-form" onSubmit={this.createNewDash}>
<div className="Form-inputs">
<div>
<FormField
name="name"
displayName={t`Name`}
......
......@@ -8,11 +8,15 @@ export default class ModalContent extends Component {
id: PropTypes.string,
title: PropTypes.string,
onClose: PropTypes.func.isRequired,
// takes over the entire screen
fullPageModal: PropTypes.bool,
// standard modal
formModal: PropTypes.bool,
};
static defaultProps = {};
static defaultProps = {
formModal: true,
};
render() {
const {
......@@ -32,6 +36,8 @@ export default class ModalContent extends Component {
"ModalContent NewForm flex-full flex flex-column relative",
className,
{ "full-height": fullPageModal && !formModal },
// add bottom padding if this is a standard "form modal" with no footer
{ pb4: formModal && !footer },
)}
>
{onClose && (
......@@ -84,7 +90,7 @@ export const ModalBody = ({ children, fullPageModal, formModal }) => (
})}
>
<div
className="ml-auto mr-auto flex flex-column"
className="flex-full ml-auto mr-auto flex flex-column"
style={{ maxWidth: formModal && fullPageModal ? FORM_WIDTH : undefined }}
>
{children}
......@@ -95,18 +101,14 @@ export const ModalBody = ({ children, fullPageModal, formModal }) => (
export const ModalFooter = ({ children, fullPageModal, formModal }) => (
<div
className={cx(
"ModalFooter flex-no-shrink px4",
fullPageModal ? "py4" : "py2",
{
"border-top": !fullPageModal || (fullPageModal && !formModal),
},
"ModalFooter flex flex-no-shrink px4",
fullPageModal ? "py4" : "py3",
)}
>
<div
className="flex-full ml-auto mr-auto flex align-center"
className="ml-auto flex align-center"
style={{ maxWidth: formModal && fullPageModal ? FORM_WIDTH : undefined }}
>
<div className="flex-full" />
{Array.isArray(children)
? children.map((child, index) => (
<span key={index} className="ml2">
......
......@@ -21,7 +21,7 @@ const ModalWithRoute = ComposedModal =>
render() {
return (
<Modal isOpen={true}>
<Modal onClose={this.onClose}>
<ComposedModal onClose={this.onClose} />
</Modal>
);
......
......@@ -7,6 +7,16 @@ import Question from "metabase-lib/lib/Question";
export const activity = "/activity";
export const newQuestion = () => "/question/new";
export const newDashboard = collectionId =>
`collection/${collectionId}/new_dashboard`;
export const newPulse = collectionId =>
`/pulse/create?collectionId=${collectionId}`;
export const newCollection = collectionId =>
`collection/${collectionId}/new_collection`;
export function question(cardId, hash = "", query = "") {
if (hash && typeof hash === "object") {
hash = serializeCardForUrl(hash);
......
......@@ -9,23 +9,13 @@ import { space, width } from "styled-system";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { createDashboard } from "metabase/dashboards/dashboards";
import { normal, saturated } from "metabase/lib/colors";
import Button from "metabase/components/Button.jsx";
import Icon from "metabase/components/Icon.jsx";
import Link from "metabase/components/Link";
import LogoIcon from "metabase/components/LogoIcon.jsx";
import Tooltip from "metabase/components/Tooltip";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
import Modal from "metabase/components/Modal";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import CollectionEdit from "metabase/questions/containers/CollectionCreate";
import ProfileLink from "metabase/nav/components/ProfileLink.jsx";
import { getPath, getContext, getUser } from "../selectors";
......@@ -38,7 +28,6 @@ const mapStateToProps = (state, props) => ({
const mapDispatchToProps = {
onChangeLocation: push,
createDashboard,
};
const AdminNavItem = ({ name, path, currentPath }) => (
......@@ -134,9 +123,6 @@ class SearchBar extends React.Component {
}
}
const MODAL_NEW_DASHBOARD = "MODAL_NEW_DASHBOARD";
const MODAL_NEW_COLLECTION = "MODAL_NEW_COLLECTION";
@connect(mapStateToProps, mapDispatchToProps)
export default class Navbar extends Component {
state = {
......@@ -208,7 +194,6 @@ export default class Navbar extends Component {
<ProfileLink {...this.props} />
</div>
{this.renderModal()}
</nav>
);
}
......@@ -227,14 +212,13 @@ export default class Navbar extends Component {
</Link>
</li>
</ul>
{this.renderModal()}
</nav>
);
}
renderMainNav() {
return (
<Flex className="relative bg-brand text-white z4" align="center">
<Flex className="Nav relative bg-brand text-white z4" align="center">
<Box>
<Link
to="/"
......@@ -256,103 +240,33 @@ export default class Navbar extends Component {
/>
</Box>
</Flex>
<Flex ml="auto" align="center" className="relative z2">
<PopoverWithTrigger
ref={e => (this._newPopover = e)}
triggerElement={
<Button medium mr={3} className="text-brand">
New
</Button>
}
>
<Box py={2} px={3} style={{ minWidth: 300 }}>
<Box my={2}>
<Link to="question/new">
<Flex align="center" style={{ color: normal.red }}>
<Icon name="beaker" mr={1} />
<h3>Question</h3>
</Flex>
</Link>
</Box>
<Box my={2}>
<Flex
align="center"
style={{ color: normal.blue }}
className="cursor-pointer"
onClick={() => this.setModal(MODAL_NEW_DASHBOARD)}
>
<Icon name="dashboard" mr={1} />
<h3>Dashboard</h3>
</Flex>
</Box>
<Box my={2}>
<Link to="pulse/new">
<Flex align="center" style={{ color: saturated.yellow }}>
<Icon name="pulse" mr={1} />
<h3>Pulse</h3>
</Flex>
</Link>
</Box>
<Box my={2}>
<Flex
align="center"
style={{ color: "#93B3C9" }}
className="cursor-pointer"
onClick={() => this.setModal(MODAL_NEW_COLLECTION)}
>
<Icon name="all" mr={1} />
<h3>Collection</h3>
</Flex>
</Box>
<Flex align="center" ml="auto" className="z4">
<Link to="question/new" mx={1}>
<Button medium color="#509ee3">
New question
</Button>
</Link>
<Link to="collection/root" mx={1}>
<Box p={1} bg="#69ABE6" className="text-bold rounded">
Saved items
</Box>
</PopoverWithTrigger>
<Box>
<Link to="collection/root">
<Box p={1} className="nav-light text-bold rounded">
Saved items
</Box>
</Link>
<Tooltip tooltip={t`Reference`}>
<Link to="reference" mx={1}>
<Icon name="reference" />
</Link>
</Box>
<Box mx={2}>
<Tooltip tooltip={t`Reference`}>
<Link to="reference">
<Icon name="reference" />
</Link>
</Tooltip>
</Box>
<Box mx={2}>
<Tooltip tooltip={t`Activity`}>
<Link to="activity">
<Icon name="alert" />
</Link>
</Tooltip>
</Box>
</Tooltip>
<Tooltip tooltip={t`Activity`}>
<Link to="activity" mx={1}>
<Icon name="alert" />
</Link>
</Tooltip>
<ProfileLink {...this.props} />
</Flex>
{this.renderModal()}
</Flex>
);
}
renderModal() {
const { modal } = this.state;
if (modal) {
return (
<Modal onClose={() => this.setState({ modal: null })}>
{modal === MODAL_NEW_COLLECTION ? (
<CollectionEdit />
) : modal === MODAL_NEW_DASHBOARD ? (
<CreateDashboardModal
createDashboard={this.props.createDashboard}
/>
) : null}
</Modal>
);
} else {
return null;
}
}
render() {
const { context, user } = this.props;
......
......@@ -113,7 +113,7 @@ export default class SharingPane extends Component {
>
<Icon name="link" size={32} />
</div>
<div className="ml2 flex-full">
<div className="ml2">
<h3 className="text-brand mb1">{t`Public link`}</h3>
<div className="mb1">{t`Share this ${resourceType} with people who don't have a Metabase account using the URL below:`}</div>
<CopyWidget value={publicLink} />
......@@ -154,7 +154,7 @@ export default class SharingPane extends Component {
src="app/assets/img/simple_embed.png"
forceOriginalDimensions={false}
/>
<div className="ml2 flex-full">
<div className="ml2">
<h3 className="text-green mb1">{t`Public embed`}</h3>
<div className="mb1">{t`Embed this ${resourceType} in blog posts or web pages by copying and pasting this snippet:`}</div>
<CopyWidget value={iframeSource} />
......@@ -172,7 +172,7 @@ export default class SharingPane extends Component {
src="app/assets/img/secure_embed.png"
forceOriginalDimensions={false}
/>
<div className="ml2 flex-full">
<div className="ml2">
<h3 className="text-purple mb1">{t`Embed this ${resourceType} in an application`}</h3>
<div className="">{t`By integrating with your application server code, you can provide a secure stats ${resourceType} limited to a specific user, customer, organization, etc.`}</div>
</div>
......
......@@ -68,7 +68,7 @@ export default class PulseCardPreview extends Component {
cardPreview.pulse_card_type == null;
return (
<div
className="flex relative flex-full"
className="relative full"
style={{
maxWidth: 379,
}}
......@@ -110,7 +110,7 @@ export default class PulseCardPreview extends Component {
/>
</div>
<div
className="bordered rounded flex-full scroll-x"
className="bordered rounded bg-white scroll-x"
style={{ display: !cardPreview && "none" }}
>
{/* Override backend rendering if pulse_card_type == null */}
......
/* eslint "react/prop-types": "warn" */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import { t, jt, ngettext, msgid } from "c-3po";
import { withRouter } from "react-router";
import PulseEditName from "./PulseEditName.jsx";
import PulseEditCollection from "./PulseEditCollection";
......@@ -12,6 +12,7 @@ import PulseEditSkip from "./PulseEditSkip.jsx";
import WhatsAPulse from "./WhatsAPulse.jsx";
import ActionButton from "metabase/components/ActionButton.jsx";
import Link from "metabase/components/Link";
import MetabaseAnalytics from "metabase/lib/analytics";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import ModalContent from "metabase/components/ModalContent.jsx";
......@@ -23,6 +24,7 @@ import * as Urls from "metabase/lib/urls";
import _ from "underscore";
import cx from "classnames";
@withRouter
export default class PulseEdit extends Component {
constructor(props) {
super(props);
......@@ -42,6 +44,7 @@ export default class PulseEdit extends Component {
saveEditingPulse: PropTypes.func.isRequired,
deletePulse: PropTypes.func.isRequired,
onChangeLocation: PropTypes.func.isRequired,
location: PropTypes.object,
};
componentDidMount() {
......@@ -116,7 +119,7 @@ export default class PulseEdit extends Component {
}
render() {
const { pulse, formInput } = this.props;
const { pulse, formInput, location } = this.props;
const isValid = pulseIsValid(pulse, formInput.channels);
const attachmentsEnabled = emailIsEnabled(pulse);
return (
......@@ -145,7 +148,11 @@ export default class PulseEdit extends Component {
</div>
<div className="PulseEdit-content pt2 pb4">
<PulseEditName {...this.props} setPulse={this.setPulse} />
<PulseEditCollection {...this.props} setPulse={this.setPulse} />
<PulseEditCollection
{...this.props}
setPulse={this.setPulse}
initialCollectionId={location.query.collectionId}
/>
<PulseEditCards
{...this.props}
setPulse={this.setPulse}
......@@ -200,7 +207,10 @@ export default class PulseEdit extends Component {
failedText={t`Save failed`}
successText={t`Saved`}
/>
<Link to="/pulse" className="Button ml2">{t`Cancel`}</Link>
<Link
to={Urls.collection(location.query.collectionId)}
className="Button ml2"
>{t`Cancel`}</Link>
</div>
</div>
);
......
......@@ -17,7 +17,11 @@ export default class PulseEditCollection extends React.Component {
return (
<Box my={2} width={400}>
<Select
value={this.props.pulse.collection_id}
placeholder="Select a collection"
value={
this.props.pulse.collection_id ||
parseInt(this.props.initialCollectionId)
}
onChange={({ target }) =>
this.props.setPulse({
...this.props.pulse,
......
......@@ -21,6 +21,7 @@ export default class CollectionCreate extends Component {
const created = await this.props.createCollection(values);
this.props.push(`/collection/${created.payload.result}`);
}}
{...this.props}
/>
);
}
......
......@@ -28,7 +28,8 @@ const formConfig = {
},
initialValues: {
name: "",
description: "",
// the api expects nil or a non blank string for collection descriptions
description: null,
// pick a random color to start so everything isn't blue all the time
color: getRandomColor(normal),
},
......
......@@ -55,6 +55,7 @@ import {
NewQuestionMetricSearch,
} from "metabase/new_query/router_wrappers";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import NotFound from "metabase/components/NotFound.jsx";
import Unauthorized from "metabase/components/Unauthorized.jsx";
......@@ -199,6 +200,8 @@ export const getRoutes = store => (
<Route path="collection/:collectionId" component={CollectionLanding}>
<ModalRoute path="archive" modal={ArchiveCollectionModal} />
<ModalRoute path="new_collection" modal={CollectionCreate} />
<ModalRoute path="new_dashboard" modal={CreateDashboardModal} />
</Route>
<Route path="activity" component={HomepageApp} />
......
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