Skip to content
Snippets Groups Projects
Commit 530f199d authored by Simon Belak's avatar Simon Belak
Browse files

Merge branch 'xray-admin-toggle' of github.com:metabase/metabase into xray-admin-toggle

parents 32d2cf28 9a009ae7
No related branches found
No related tags found
No related merge requests found
Showing
with 223 additions and 73 deletions
......@@ -39,17 +39,17 @@ export default class Mode {
return this._queryMode.name;
}
actions(): ClickAction[] {
actions(settings): ClickAction[] {
return _.flatten(
this._queryMode.actions.map(actionCreator =>
actionCreator({ question: this._question }))
actionCreator({ question: this._question, settings }))
);
}
actionsForClick(clicked: ?ClickObject): ClickAction[] {
actionsForClick(clicked: ?ClickObject, settings): ClickAction[] {
return _.flatten(
this._queryMode.drills.map(actionCreator =>
actionCreator({ question: this._question, clicked }))
actionCreator({ question: this._question, settings, clicked }))
);
}
}
......@@ -7,8 +7,14 @@ import Icon from 'metabase/components/Icon'
import COSTS from 'metabase/xray/costs'
const SettingsXrayForm = ({ settings, elements, updateSetting }) => {
const maxCost = Object.assign({}, ...elements.filter(e => e.key === 'xray-max-cost',))
let maxCost = Object.assign({}, ...elements.filter(e => e.key === 'xray-max-cost',))
const enabled = Object.assign({}, ...elements.filter(e => e.key === 'enable-xrays',))
// handle the current behavior of the default
if(maxCost.value == null) {
maxCost.value = 'extended'
}
return (
<div>
<div className="mx2">
......
......@@ -31,7 +31,7 @@ export type Card = {
public_uuid: string,
// Not part of the card API contract, a field used by query builder for showing lineage
original_card_id?: CardId
original_card_id?: CardId,
};
export type StructuredDatasetQuery = {
......
......@@ -50,7 +50,8 @@ export type ClickAction = {
export type ClickActionProps = {
question: Question,
clicked?: ClickObject
clicked?: ClickObject,
settings: {}
}
export type OnChangeCardAndRun = ({ nextCard: Card, previousCard?: ?Card }) => void
......
......@@ -5,8 +5,13 @@ import type {
ClickActionProps
} from "metabase/meta/types/Visualization";
export default ({ question }: ClickActionProps): ClickAction[] => {
if (question.card().id) {
export default ({ question, settings }: ClickActionProps): ClickAction[] => {
// currently time series xrays require the maximum fidelity
if (
question.card().id &&
settings["enable_xrays"] &&
settings["xray_max_cost"] === "extended"
) {
return [
{
name: "xray-card",
......
......@@ -7,8 +7,8 @@ import type {
import { isSegmentFilter } from "metabase/lib/query/filter";
export default ({ question }: ClickActionProps): ClickAction[] => {
if (question.card().id) {
export default ({ question, settings }: ClickActionProps): ClickAction[] => {
if (question.card().id && settings["enable_xrays"]) {
return question
.query()
.filters()
......
......@@ -19,9 +19,9 @@ const SegmentMode: QueryMode = {
name: "segment",
actions: [
...DEFAULT_ACTIONS,
XRaySegment,
CommonMetricsAction,
CountByTimeAction,
XRaySegment,
SummarizeBySegmentMetricAction
// commenting this out until we sort out viz settings in QB2
// PlotSegmentField
......
......@@ -47,9 +47,9 @@ export const TimeseriesModeFooter = (props: Props) => {
const TimeseriesMode: QueryMode = {
name: "timeseries",
actions: [
XRayCard,
PivotByCategoryAction,
PivotByLocationAction,
XRayCard,
...DEFAULT_ACTIONS
],
drills: [PivotByCategoryDrill, PivotByLocationDrill, ...DEFAULT_DRILLS],
......
......@@ -22,7 +22,8 @@ type Props = {
navigateToNewCardInsideQB: (any) => void,
router: {
push: (string) => void
}
},
instanceSettings: {}
};
type State = {
......@@ -98,10 +99,10 @@ export default class ActionsWidget extends Component {
}
handleActionClick = (index: number) => {
const { question, router } = this.props;
const { question, router, instanceSettings } = this.props;
const mode = question.mode()
if (mode) {
const action = mode.actions()[index];
const action = mode.actions(instanceSettings)[index];
if (action && action.popover) {
this.setState({ selectedActionIndex: index });
} else if (action && action.question) {
......@@ -119,15 +120,16 @@ export default class ActionsWidget extends Component {
}
};
render() {
const { className, question } = this.props;
const { className, question, instanceSettings } = this.props;
const { popoverIsOpen, iconIsVisible, selectedActionIndex } = this.state;
const mode = question.mode();
const actions = mode ? mode.actions() : [];
const actions = mode ? mode.actions(instanceSettings) : [];
if (actions.length === 0) {
return null;
}
const selectedAction: ?ClickAction = selectedActionIndex == null ? null :
actions[selectedActionIndex];
let PopoverComponent = selectedAction && selectedAction.popover;
......
......@@ -46,7 +46,8 @@ import {
getMode,
getQuery,
getQuestion,
getOriginalQuestion
getOriginalQuestion,
getSettings
} from "../selectors";
import { getMetadata, getDatabasesList } from "metabase/selectors/metadata";
......@@ -116,6 +117,7 @@ const mapStateToProps = (state, props) => {
loadTableAndForeignKeysFn: loadTableAndForeignKeys,
autocompleteResultsFn: (prefix) => autocompleteResults(state.qb.card, prefix),
instanceSettings: getSettings(state)
}
}
......@@ -202,7 +204,7 @@ export default class QueryBuilder extends Component {
class LegacyQueryBuilder extends Component {
render() {
const { query, card, isDirty, databases, uiControls, mode } = this.props;
const { query, card, isDirty, databases, uiControls, mode, } = this.props;
// if we don't have a card at all or no databases then we are initializing, so keep it simple
if (!card || !databases) {
......
......@@ -29,6 +29,10 @@ export const getParameterValues = state => state.qb.parameterValues;
export const getQueryResult = state => state.qb.queryResult;
export const getQueryResults = state => state.qb.queryResults;
// get instance settings, used for determining whether to display certain actions,
// currently used only for xrays
export const getSettings = state => state.settings.values
export const getIsDirty = createSelector(
[getCard, getOriginalCard],
(card, originalCard) => {
......
......@@ -19,13 +19,16 @@ import {
getIsEditing
} from '../selectors';
import { getXrayEnabled } from 'metabase/xray/selectors'
const mapStateToProps = (state, props) => ({
database: getDatabase(state, props),
table: getTable(state, props),
field: getField(state, props),
database: getDatabase(state, props),
table: getTable(state, props),
field: getField(state, props),
databaseId: getDatabaseId(state, props),
isEditing: getIsEditing(state, props),
metadata: getMetadata(state, props)
metadata: getMetadata(state, props),
showXray: getXrayEnabled(state)
});
const mapDispatchToProps = {
......@@ -43,7 +46,8 @@ export default class FieldDetailContainer extends Component {
table: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
isEditing: PropTypes.bool,
metadata: PropTypes.object
metadata: PropTypes.object,
showXray: PropTypes.bool
};
async fetchContainerData(){
......@@ -68,14 +72,15 @@ export default class FieldDetailContainer extends Component {
database,
table,
field,
isEditing
isEditing,
showXray
} = this.props;
return (
<SidebarLayout
className="flex-full relative"
style={ isEditing ? { paddingTop: '43px' } : {}}
sidebar={<FieldSidebar database={database} table={table} field={field}/>}
sidebar={<FieldSidebar database={database} table={table} field={field} showXray={showXray}/>}
>
<FieldDetail {...this.props} />
</SidebarLayout>
......
......@@ -14,7 +14,8 @@ const FieldSidebar =({
table,
field,
style,
className
className,
showXray
}) =>
<div className={cx(S.sidebar, className)} style={style}>
<ul>
......@@ -28,23 +29,26 @@ const FieldSidebar =({
placeholder="Data Reference"
/>
</div>
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
href={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
icon="document"
name="Details" />
<SidebarItem key={`/xray/field/${field.id}/approximate`}
href={`/xray/field/${field.id}/approximate`}
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
href={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
icon="document"
name="X-Ray this Field" />
name="Details" />
{ showXray && (
<SidebarItem key={`/xray/field/${field.id}/approximate`}
href={`/xray/field/${field.id}/approximate`}
icon="beaker"
name="X-Ray this Field" />
)}
</ul>
</div>
FieldSidebar.propTypes = {
database: PropTypes.object,
database: PropTypes.object,
table: PropTypes.object,
field: PropTypes.object,
className: PropTypes.string,
style: PropTypes.object,
showXray: PropTypes.bool
};
export default pure(FieldSidebar);
......
......@@ -17,12 +17,15 @@ import {
getIsEditing
} from '../selectors';
import { getXrayEnabled } from 'metabase/xray/selectors'
const mapStateToProps = (state, props) => ({
database: getDatabase(state, props),
table: getTable(state, props),
database: getDatabase(state, props),
table: getTable(state, props),
databaseId: getDatabaseId(state, props),
isEditing: getIsEditing(state, props)
isEditing: getIsEditing(state, props),
showXray: getXrayEnabled(state)
});
const mapDispatchToProps = {
......@@ -38,7 +41,8 @@ export default class TableDetailContainer extends Component {
database: PropTypes.object.isRequired,
databaseId: PropTypes.number.isRequired,
table: PropTypes.object.isRequired,
isEditing: PropTypes.bool
isEditing: PropTypes.bool,
showXray: PropTypes.bool
};
async fetchContainerData(){
......@@ -62,14 +66,16 @@ export default class TableDetailContainer extends Component {
const {
database,
table,
isEditing
isEditing,
showXray
} = this.props;
return (
<SidebarLayout
className="flex-full relative"
style={ isEditing ? { paddingTop: '43px' } : {}}
sidebar={<TableSidebar database={database} table={table}/>}
sidebar={<TableSidebar database={database} table={table} showXray={showXray}/>}
>
<TableDetail {...this.props} />
</SidebarLayout>
......
......@@ -13,7 +13,8 @@ const TableSidebar = ({
database,
table,
style,
className
className,
showXray
}) =>
<div className={cx(S.sidebar, className)} style={style}>
<div className={S.breadcrumbs}>
......@@ -27,30 +28,33 @@ const TableSidebar = ({
/>
</div>
<ol>
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}`}
href={`/reference/databases/${database.id}/tables/${table.id}`}
icon="document"
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}`}
href={`/reference/databases/${database.id}/tables/${table.id}`}
icon="document"
name="Details" />
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields`}
href={`/reference/databases/${database.id}/tables/${table.id}/fields`}
icon="fields"
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields`}
href={`/reference/databases/${database.id}/tables/${table.id}/fields`}
icon="fields"
name="Fields in this table" />
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/questions`}
href={`/reference/databases/${database.id}/tables/${table.id}/questions`}
icon="all"
name="Questions about this table" />
<SidebarItem key={`/xray/table/${table.id}/approximate`}
href={`/xray/table/${table.id}/approximate`}
<SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/questions`}
href={`/reference/databases/${database.id}/tables/${table.id}/questions`}
icon="all"
name="X-Ray this table" />
name="Questions about this table" />
{ showXray && (
<SidebarItem key={`/xray/table/${table.id}/approximate`}
href={`/xray/table/${table.id}/approximate`}
icon="beaker"
name="X-Ray this table" />
)}
</ol>
</div>
TableSidebar.propTypes = {
database: PropTypes.object,
database: PropTypes.object,
table: PropTypes.object,
className: PropTypes.string,
style: PropTypes.object,
showXray: PropTypes.bool
};
export default pure(TableSidebar);
......
......@@ -204,7 +204,7 @@ export default class Visualization extends Component {
const card = series[seriesIndex].card;
const question = new Question(metadata, card);
const mode = question.mode();
return mode ? mode.actionsForClick(clicked) : [];
return mode ? mode.actionsForClick({}, clicked) : [];
}
visualizationIsClickable = (clicked: ClickObject) => {
......@@ -213,7 +213,7 @@ export default class Visualization extends Component {
return false;
}
try {
return this.getClickActions(clicked).length > 0;
return this.getClickActions({}, clicked).length > 0;
} catch (e) {
console.warn(e);
return false;
......
......@@ -19,6 +19,7 @@ const getDisabled = (maxCost) => {
} else if (maxCost === 'exact') {
return ['extended']
}
return []
}
const CostSelect = ({ currentCost, location, maxCost }) => {
......@@ -30,7 +31,10 @@ const CostSelect = ({ currentCost, location, maxCost }) => {
return (
<Link
to={`${urlWithoutCost}/${cost}`}
className="no-decoration"
className={cx(
'no-decoration',
{ 'disabled': getDisabled(maxCost).indexOf(cost) >= 0}
)}
key={cost}
>
<li
......@@ -38,7 +42,6 @@ const CostSelect = ({ currentCost, location, maxCost }) => {
className={cx(
"flex align-center justify-center cursor-pointer bg-brand-hover text-white-hover transition-background transition-text text-grey-2",
{ 'bg-brand text-white': currentCost === cost },
{ 'disabled': getDisabled(maxCost).indexOf(cost) > 0 }
)}
>
<Tooltip
......
......@@ -114,5 +114,14 @@ export const getTableItem = (state, index = 1) => createSelector(
export const getComparisonForField = createSelector
export const getMaxCost = state => state.settings.values['xray-max-cost']
// see if xrays are enabled. unfortunately enabled can equal null so its enabled if its not false
export const getXrayEnabled = state => {
const enabled = state.settings.values && state.settings.values['enable_xrays']
if(enabled == null || enabled == true) {
return true
}
return false
}
export const getMaxCost = state => state.settings.values['xray_max_cost']
......@@ -8,7 +8,7 @@ import {
} from "__support__/enzyme_utils"
import { mount } from "enzyme";
import { CardApi, SegmentApi } from "metabase/services";
import { CardApi, SegmentApi, SettingsApi } from "metabase/services";
import { delay } from "metabase/lib/promise";
import { FETCH_CARD_XRAY, FETCH_SEGMENT_XRAY, FETCH_TABLE_XRAY } from "metabase/xray/xray";
......@@ -25,6 +25,9 @@ import ActionsWidget from "metabase/query_builder/components/ActionsWidget";
// settings related actions for testing xray administration
import { INITIALIZE_SETTINGS, UPDATE_SETTING } from "metabase/admin/settings/settings";
import { LOAD_CURRENT_USER } from "metabase/redux/user";
import { END_LOADING } from "metabase/reference/reference";
import { getXrayEnabled, getMaxCost } from "metabase/xray/selectors";
import Icon from "metabase/components/Icon"
import Toggle from "metabase/components/Toggle"
......@@ -66,10 +69,11 @@ describe("xray integration tests", () => {
await SegmentApi.delete({ segmentId, revision_message: "Sadly this segment didn't enjoy a long life either" })
await CardApi.delete({cardId: timeBreakoutQuestion.id()})
await CardApi.delete({cardId: segmentQuestion.id()})
await SettingsApi.put({ key: 'enable-xrays' }, true)
})
describe("for table xray", async () => {
xit("should render the table xray page without errors", async () => {
it("should render the table xray page without errors", async () => {
const store = await createTestStore()
store.pushPath(`/xray/table/1/approximate`);
......@@ -88,8 +92,11 @@ describe("xray integration tests", () => {
// in the same tests so that we see that end-to-end user experience matches our expectations
describe("query builder actions", async () => {
xit("let you see card xray for a timeseries question", async () => {
it("let you see card xray for a timeseries question", async () => {
await SettingsApi.put({ key: 'enable-xrays', value: 'true' })
await SettingsApi.put({ key: 'xray-max-cost', value: 'extended' })
const store = await createTestStore()
// make sure xrays are on and at the proper cost
store.pushPath(Urls.question(timeBreakoutQuestion.id()))
const app = mount(store.getAppContainer());
......@@ -111,7 +118,8 @@ describe("xray integration tests", () => {
expect(cardXRay.text()).toMatch(/Time breakout question/);
})
xit("let you see segment xray for a question containing a segment", async () => {
it("let you see segment xray for a question containing a segment", async () => {
await SettingsApi.put({ key: 'enable-xrays', value: true })
const store = await createTestStore()
store.pushPath(Urls.question(segmentQuestion.id()))
const app = mount(store.getAppContainer());
......@@ -151,25 +159,52 @@ describe("xray integration tests", () => {
// there should be a toggle
expect(xrayToggle.length).toEqual(1)
// things should be on
expect(getXrayEnabled(store.getState())).toEqual(true)
// the toggle should be on by default
expect(xrayToggle.props().value).toEqual(true)
// toggle the... toggle
click(xrayToggle)
expect(xrayToggle.props().value).toEqual(false)
await store.waitForActions([UPDATE_SETTING])
expect(getXrayEnabled(store.getState())).toEqual(false)
// navigate to a previosuly x-ray-able entity
store.pushPath(Urls.question(timeBreakoutQuestion.id()))
await store.waitForActions(INITIALIZE_QB, QUERY_COMPLETED)
app = mount(store.getAppContainer())
// for some reason a delay is needed to get the full action suite
await delay(500);
const actionsWidget = app.find(ActionsWidget)
click(actionsWidget.childAt(0))
// there should not be an xray option
const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
expect(xrayOptionIcon.length).toEqual(0)
})
it("should not show xray options for segments when xrays are disabled", async () => {
// turn off xrays
await SettingsApi.put({ key: 'enable-xrays', value: false })
const store = await createTestStore()
store.pushPath(Urls.question(segmentQuestion.id()))
const app = mount(store.getAppContainer())
await store.waitForActions(INITIALIZE_QB, QUERY_COMPLETED)
await delay(500);
console.log('post admin', app.debug())
const actionsWidget = app.find(ActionsWidget)
click(actionsWidget.childAt(0))
const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
expect(xrayOptionIcon.length).toEqual(0)
})
it("should let an admin set the max cost of xrays", async () => {
it("should properly reflect the an admin set the max cost of xrays", async () => {
await SettingsApi.put({ key: 'enable-xrays', value: true })
const store = await createTestStore()
store.pushPath('/admin/settings/x_rays')
......@@ -182,6 +217,68 @@ describe("xray integration tests", () => {
expect(xraySettings.find(Icon).length).toEqual(3)
const approximate = xraySettings.find('.text-measure li').first()
click(approximate)
await store.waitForActions([UPDATE_SETTING])
expect(approximate.hasClass('text-brand')).toEqual(true)
expect(getMaxCost(store.getState())).toEqual('approximate')
store.pushPath(`/xray/table/1/approximate`);
await store.waitForActions(FETCH_TABLE_XRAY, { timeout: 20000 })
await delay(200)
const tableXRay = app.find(TableXRay)
expect(tableXRay.length).toBe(1)
expect(tableXRay.find(CostSelect).length).toBe(1)
// there should be two disabled states
expect(tableXRay.find('a.disabled').length).toEqual(2)
})
})
describe("data reference entry", async () => {
it("should be possible to access an Xray from the data reference", async () => {
// ensure xrays are on
await SettingsApi.put({ key: 'enable-xrays', value: true })
const store = await createTestStore()
store.pushPath('/reference/databases/1/tables/1')
const app = mount(store.getAppContainer())
await store.waitForActions([END_LOADING])
const xrayTableSideBarItem = app.find('.Icon.Icon-beaker')
expect(xrayTableSideBarItem.length).toEqual(1)
store.pushPath('/reference/databases/1/tables/1/fields/1')
await store.waitForActions([END_LOADING])
const xrayFieldSideBarItem = app.find('.Icon.Icon-beaker')
expect(xrayFieldSideBarItem.length).toEqual(1)
})
it("should not be possible to access an Xray from the data reference if xrays are disabled", async () => {
// turn off xrays
await SettingsApi.put({ key: 'enable-xrays', value: false })
const store = await createTestStore()
const app = mount(store.getAppContainer())
store.pushPath('/reference/databases/1/tables/1')
await store.waitForActions([END_LOADING])
const xrayTableSideBarItem = app.find('.Icon.Icon-beaker')
expect(xrayTableSideBarItem.length).toEqual(0)
store.pushPath('/reference/databases/1/tables/1/fields/1')
await store.waitForActions([END_LOADING])
const xrayFieldSideBarItem = app.find('.Icon.Icon-beaker')
expect(xrayFieldSideBarItem.length).toEqual(0)
})
})
......
......@@ -137,6 +137,7 @@
:embedding (enable-embedding)
:enable_query_caching (enable-query-caching)
:enable_nested_queries (enable-nested-queries)
:enable_xrays (setting/get :enable-xrays)
:engines ((resolve 'metabase.driver/available-drivers))
:ga_code "UA-60817802-1"
:google_auth_client_id (setting/get :google-auth-client-id)
......@@ -152,4 +153,5 @@
:timezone_short (short-timezone-name (setting/get :report-timezone))
:timezones common/timezones
:types (types/types->parents)
:version config/mb-version-info})
:version config/mb-version-info
:xray_max_cost (setting/get :xray-max-cost)})
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