Skip to content
Snippets Groups Projects
Commit a8d25848 authored by Atte Keinänen's avatar Atte Keinänen
Browse files

Merge branch 'master' of https://github.com/metabase/metabase into smarter-xray-comparisons-page

parents 9098c38e c9fb3407
No related branches found
No related tags found
No related merge requests found
Showing
with 552 additions and 28 deletions
......@@ -47,3 +47,4 @@ bin/release/aws-eb/metabase-aws-eb.zip
/crate-*
*.po~
/locales/metabase-*.pot
/stats.json
#!/usr/bin/env bash
VERSION="v0.26.1"
VERSION="v0.27.0-snapshot"
# dynamically pull more interesting stuff from latest git commit
HASH=$(git show-ref --head --hash=7 head) # first 7 letters of hash should be enough; that's what GitHub uses
......
......@@ -25,7 +25,7 @@ To persist your data outside of the container and make it available for use betw
docker run -d -p 3000:3000 -v ~/metabase-data:/metabase-data -e "MB_DB_FILE=/metabase-data/metabase.db" --name metabase metabase/metabase
Now when you launch your container we are telling Metabase to use the database file at `/tmp/metabase.db` instead of its default location and we are mounting that folder from our local filesystem into the container.
Now when you launch your container we are telling Metabase to use the database file at `~/metabase-data/metabase.db` instead of its default location and we are mounting that folder from our local filesystem into the container.
### Getting your config back if you stopped your container
......
import React from 'react'
const Card = ({ children }) =>
<div className="bordered rounded shadowed bg-white">
{ children }
</div>
export default Card
import React from 'react'
import EntityMenu from 'metabase/components/EntityMenu'
export const component = EntityMenu
export const description = `
A menu with varios entity related options grouped by context.
`
const DemoAlignRight = ({ children }) =>
<div className="flex flex-full">
<div className="flex align-center ml-auto">
{children}
</div>
</div>
export const examples = {
'Edit menu': (
<DemoAlignRight>
<EntityMenu
triggerIcon='pencil'
items={[
{ title: "Edit this question", icon: "editdocument", action: () => alert('Action type') },
{ title: "View revision history", icon: "history", link: '/derp' },
{ title: "Move", icon: "move", action: () => alert('Move action') },
{ title: "Archive", icon: "archive", action: () => alert('Archive action') }
]}
/>
</DemoAlignRight>
),
'Share menu': (
<DemoAlignRight>
<EntityMenu
triggerIcon='share'
items={[
{ title: "Add to dashboard", icon: "addtodash", action: () => alert('Action type') },
{ title: "Download results", icon: "download", link: '/download' },
{ title: "Sharing and embedding", icon: "embed", action: () => alert('Another action type') },
]}
/>
</DemoAlignRight>
),
'More menu': (
<DemoAlignRight>
<EntityMenu
triggerIcon='burger'
items={[
{ title: "Get alerts about this", icon: "alert", action: () => alert('Get alerts about this') },
{ title: "View the SQL", icon: "sql", link: '/download' },
]}
/>
</DemoAlignRight>
),
'Multiple menus': (
<DemoAlignRight>
<EntityMenu
triggerIcon='pencil'
items={[
{ title: "Edit this question", icon: "editdocument", action: () => alert('Action type') },
{ title: "View revision history", icon: "history", link: '/derp' },
{ title: "Move", icon: "move", action: () => alert('Move action') },
{ title: "Archive", icon: "archive", action: () => alert('Archive action') }
]}
/>
<EntityMenu
triggerIcon='share'
items={[
{ title: "Add to dashboard", icon: "addtodash", action: () => alert('Action type') },
{ title: "Download results", icon: "download", link: '/download' },
{ title: "Sharing and embedding", icon: "embed", action: () => alert('Another action type') },
]}
/>
<EntityMenu
triggerIcon='burger'
items={[
{ title: "Get alerts about this", icon: "alert", action: () => alert('Get alerts about this') },
{ title: "View the SQL", icon: "sql", link: '/download' },
]}
/>
</DemoAlignRight>
)
}
/* @flow */
import React, { Component } from 'react'
import { Motion, spring } from 'react-motion'
import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper'
import Card from 'metabase/components/Card'
import EntityMenuTrigger from 'metabase/components/EntityMenuTrigger'
import EntityMenuItem from 'metabase/components/EntityMenuItem'
type EntityMenuOption = {
icon: string,
title: string,
action?: () => void,
link?: string
}
type Props = {
items: Array<EntityMenuOption>,
triggerIcon: string
}
class EntityMenu extends Component {
props: Props
state = {
open: false
}
toggleMenu = () => {
const open = !this.state.open
this.setState({ open })
}
render () {
const { items, triggerIcon } = this.props
const { open } = this.state
return (
<div className="relative">
<EntityMenuTrigger
icon={triggerIcon}
onClick={this.toggleMenu}
open={open}
/>
{ open && (
/* Note: @kdoh 10/12/17
* React Motion has a flow type problem with children see
* https://github.com/chenglou/react-motion/issues/375
* TODO This can be removed if we upgrade to flow 0.53 and react-motion >= 0.5.1
* $FlowFixMe */
<Motion
defaultStyle={{
opacity: 0,
translateY: 0
}}
style={{
opacity: open ? spring(1): spring(0),
translateY: open ? spring(10) : spring(0)
}}
>
{ ({ opacity, translateY }) =>
<OnClickOutsideWrapper handleDismissal={this.toggleMenu}>
<div
className="absolute right"
style={{
top: 35,
opacity: opacity,
transform: `translateY(${translateY}px)`
}}
>
<Card>
<ol className="py1" style={{ minWidth: 210 }}>
{items.map(item => {
return (
<li key={item.title}>
<EntityMenuItem
icon={item.icon}
title={item.title}
action={item.action}
link={item.link}
/>
</li>
)
})}
</ol>
</Card>
</div>
</OnClickOutsideWrapper>
}
</Motion>
)}
</div>
)
}
}
export default EntityMenu
import cxs from 'cxs'
import React from 'react'
import { Link } from 'react-router'
import Icon from 'metabase/components/Icon'
const itemClasses = cxs({
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
color: '#616D75',
paddingLeft: '1.45em',
paddingRight: '1.45em',
paddingTop: '0.85em',
paddingBottom: '0.85em',
textDecoration: 'none',
transition: 'all 300ms linear',
':hover': {
color: '#509ee3'
},
'> .Icon': {
color: '#BCC5CA',
marginRight: '0.65em'
},
':hover > .Icon': {
color: '#509ee3',
transition: 'all 300ms linear',
},
// icon specific tweaks
// the alert icon should be optically aligned with the x-height of the text
'> .Icon.Icon-alert': {
transform: `translateY(1px)`
},
// the embed icon should be optically aligned with the x-height of the text
'> .Icon.Icon-embed': {
transform: `translateY(1px)`
},
// the download icon should be optically aligned with the x-height of the text
'> .Icon.Icon-download': {
transform: `translateY(1px)`
},
// the history icon is wider so it needs adjustement to center it with other
// icons
'> .Icon.Icon-history': {
transform: `translateX(-2px)`
}
})
const LinkMenuItem = ({ children, link }) =>
<Link className={itemClasses} to={link}>
{children}
</Link>
const ActionMenuItem = ({ children, action }) =>
<div className={itemClasses} onClick={action}>
{children}
</div>
const EntityMenuItem = ({
action,
title,
icon,
link
}) => {
if(link && action) {
console.warn('EntityMenuItem Error: You cannot specify both action and link props')
return <div></div>
}
const content = [
<Icon name={icon} />,
<span className="text-bold">{title}</span>
]
if(link) {
return (
<LinkMenuItem link={link}>
{content}
</LinkMenuItem>
)
}
if(action) {
return (
<ActionMenuItem action={action}>
{content}
</ActionMenuItem>
)
}
}
export default EntityMenuItem
import React from 'react'
import Icon from 'metabase/components/Icon'
import cxs from 'cxs'
const EntityzMenuTrigger = ({ icon, onClick, open }) => {
const interactionColor = '#F2F4F5'
const classes = cxs({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 99,
cursor: 'pointer',
color: open ? '#509ee3' : 'inherit',
backgroundColor: open ? interactionColor : 'transparent',
':hover': {
backgroundColor: interactionColor,
color: '#509ee3',
transition: 'all 300ms linear'
},
// special cases for certain icons
// Icon-share has a taller viewvbox than most so to optically center
// the icon we need to translate it upwards
'> .Icon.Icon-share': {
transform: `translateY(-2px)`
}
})
return (
<div onClick={onClick} className={classes}>
<Icon name={icon} className="m1" />
</div>
)
}
export default EntityzMenuTrigger
......@@ -62,7 +62,7 @@ export default class NewsletterForm extends Component {
<div className="MB-Newsletter sm-float-right">
<div>
<div style={{color: "#878E95"}} className="text-grey-4 text-strong h3 pb3">
<div style={{color: "#878E95"}} className="text-grey-4 h3 pb3">
Get infrequent emails about new releases and feature updates.
</div>
......
......@@ -32,7 +32,7 @@
.Form-field .Form-label {
display: block;
font-size: 0.85em;
font-weight: bold;
font-weight: 700;
color: currentColor;
}
......
......@@ -10,15 +10,16 @@ html {
}
body {
font-family: var(--default-font-family), "Helvetica Neue", Helvetica, sans-serif;
font-family: var(--default-font-family), sans-serif;
font-size: var(--default-font-size);
font-weight: 400;
font-style: normal;
color: var(--default-font-color);
margin: 0;
height: 100%; /* ensure the entire page will fill the window */
display: flex;
flex-direction: column;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
......
......@@ -8,6 +8,7 @@ h3, .h3,
h4, .h4,
h5, .h5,
h6, .h6 {
font-weight: 700;
margin-top: var(--default-header-margin);
margin-bottom: var(--default-header-margin);
}
......
......@@ -73,7 +73,6 @@
/* text weight */
.text-light { font-weight: 300; }
.text-normal { font-weight: 400; }
.text-strong { font-weight: 570; }
.text-bold, :local(.text-bold) { font-weight: 700; }
/* text style */
......
......@@ -31,6 +31,7 @@
border-right: none;
}
/*
.Icon-download,
.Icon-addToDash {
fill: #919191;
......@@ -42,6 +43,7 @@
fill: var(--brand-color);
transition: fill .3s linear;
}
*/
/* a section of the graphical query itself */
.Query-section {
......
This diff is collapsed.
......@@ -35,7 +35,7 @@ export default class ComponentsApp extends Component {
<div
className="p2 bordered flex align-center flex-full"
>
<div>
<div className="full">
{element}
</div>
</div>
......
import { AsyncApi } from "metabase/services";
export class RestfulRequest {
// API endpoint that is used for the request
endpoint = null
// Prefix for request Redux actions
actionPrefix = null
// Name of the request result property
// In general, using the default value `result` is good for consistency
// but using an existing prop name (like `xray` or `dashboard`) temporarily
// can make the migration process from old implementation to this request API a lot easier
resultPropName = 'result'
constructor({ endpoint, actionPrefix, resultPropName } = {}) {
this.endpoint = endpoint
this.actionPrefix = actionPrefix
this.resultPropName = resultPropName || this.resultPropName
this.actions = {
requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
resetRequest: `${this.actionPrefix}/REQUEST_RESET`
}
}
// Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
trigger = (params) =>
async (dispatch) => {
dispatch.action(this.actions.requestStarted)
try {
const result = await this.endpoint(params)
dispatch.action(this.actions.requestSuccessful, { result })
} catch(error) {
console.error(error)
dispatch.action(this.actions.requestFailed, { error })
}
}
reset = () => (dispatch) => dispatch(this.actions.reset)
getReducers = () => ({
[this.actions.requestStarted]: (state) => ({...state, loading: true}),
[this.actions.requestSuccessful]: (state, { payload: { result }}) => ({
...state,
[this.resultPropName]: result,
loading: false,
fetched: true
}),
[this.actions.requestFailed]: (state, { payload: { error } }) => ({
...state,
loading: false,
error: error
}),
[this.actions.resetRequest]: (state) => ({ ...state, ...this.getDefaultState() })
})
getDefaultState = () => ({
[this.resultPropName]: null,
loading: false,
fetched: false,
error: null
})
}
const POLLING_INTERVAL = 100
export class BackgroundJobRequest {
// API endpoint that creates a new background job
creationEndpoint = null
// Prefix for request Redux actions
actionPrefix = null
// Name of the request result property
// In general, using the default value `result` is good for consistency
// but using an existing prop name (like `xray` or `dashboard`) temporarily
// can make the migration process from old implementation to this request API a lot easier
resultPropName = 'result'
pollingTimeoutId = null
constructor({ creationEndpoint, actionPrefix, resultPropName } = {}) {
this.creationEndpoint = creationEndpoint
this.actionPrefix = actionPrefix
this.resultPropName = resultPropName || this.resultPropName
this.actions = {
requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
resetRequest: `${this.actionPrefix}/REQUEST_RESET`
}
}
// Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
trigger = (params) => {
return async (dispatch) => {
dispatch.action(this.actions.requestStarted)
try {
const newJobId = await this._createNewJob(params)
const result = await this._pollForResult(newJobId)
dispatch.action(this.actions.requestSuccessful, { result })
} catch(error) {
console.error(error)
dispatch.action(this.actions.requestFailed, { error })
}
}
}
_createNewJob = async (requestParams) => {
return (await this.creationEndpoint(requestParams))["job-id"]
}
_pollForResult = (jobId) => {
if (this.pollingTimeoutId) {
clearTimeout(this.pollingTimeoutId);
}
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const response = await AsyncApi.status({ jobId })
if (response.status === 'done') {
resolve(response.result)
} else if (response.status === 'result-not-available') {
// The job result has been deleted; this is an unexpected state as we just
// created the job so simply throw a descriptive error
reject(new ResultNoAvailableError())
} else {
this.pollingTimeoutId = setTimeout(poll, POLLING_INTERVAL)
}
} catch (error) {
this.pollingTimeoutId = null
reject(error)
}
}
poll()
})
}
reset = () => (dispatch) => dispatch(this.actions.reset)
getReducers = () => ({
[this.actions.requestStarted]: (state) => ({...state, loading: true}),
[this.actions.requestSuccessful]: (state, { payload: { result }}) => ({
...state,
[this.resultPropName]: result,
loading: false,
fetched: true
}),
[this.actions.requestFailed]: (state, { payload: { error } }) => ({
...state,
loading: false,
error: error
}),
[this.actions.resetRequest]: (state) => ({ ...state, ...this.getDefaultState() })
})
getDefaultState = () => ({
[this.resultPropName]: null,
loading: false,
fetched: false,
error: null
})
}
class ResultNoAvailableError extends Error {
constructor() {
super()
this.message = "Background job result isn't available for an unknown reason"
}
}
......@@ -84,7 +84,7 @@ export default class Navbar extends Component {
<span className="NavItem-text ml1 hide sm-show text-bold">Metabase Admin</span>
</div>
<ul className="sm-ml4 flex flex-full text-strong">
<ul className="sm-ml4 flex flex-full">
<AdminNavItem name="Settings" path="/admin/settings" currentPath={this.props.path} />
<AdminNavItem name="People" path="/admin/people" currentPath={this.props.path} />
<AdminNavItem name="Data Model" path="/admin/datamodel" currentPath={this.props.path} />
......
......@@ -261,7 +261,7 @@ export default class QueryHeader extends Component {
buttonSections.push([
<button
key="recentlySaved"
className="cursor-pointer bg-white text-success text-strong text-uppercase"
className="cursor-pointer bg-white text-success text-bold text-uppercase"
>
<span>
<Icon name='check' size={12} />
......
......@@ -71,14 +71,14 @@ export default class ExpandingSearchField extends Component {
<div
className={cx(
className,
'bordered border-dark flex align-center pr2 transition-border',
'bordered border-grey-1 flex align-center pr2 transition-border',
{ 'border-brand' : active }
)}
onClick={this.setActive}
style={{borderRadius: 99}}
>
<Icon
className={cx('ml2', { 'text-brand': active })}
className={cx('ml2 text-grey-3', { 'text-brand': active })}
name="search"
/>
<Motion
......@@ -87,8 +87,8 @@ export default class ExpandingSearchField extends Component {
{ interpolatingStyle =>
<input
ref={(search) => this.searchInput = search}
className="input text-bold borderless"
placeholder="Search for a question..."
className="input borderless text-bold"
placeholder="Search for a question"
style={Object.assign({}, interpolatingStyle, { fontSize: '1em'})}
onFocus={() => this.setState({ active: true })}
onBlur={() => this.setState({ active: false })}
......
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