Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Adding new exploration UI #3449

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions superset/assets/backendSync.json
Original file line number Diff line number Diff line change
Expand Up @@ -2059,6 +2059,18 @@
"default": "linear",
"description": "Line interpolation as defined by d3.js"
},
"overlays": {
"type": "SelectControl",
"multi": true,
"label": "Overlays",
"default": []
},
"offset_overlays": {
"type": "CheckboxControl",
"label": "Auto Offset Overlays",
"default": false,
"description": "Auto offset overlay to match the time frame of the current config."
},
"pie_label_type": {
"type": "SelectControl",
"label": "Label Type",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import URLShortLinkButton from './URLShortLinkButton';
import EmbedCodeButton from './EmbedCodeButton';
import DisplayQueryButton from './DisplayQueryButton';
import { t } from '../../locales';
import { getSwivelUrl } from '../exploreUtils';
import { isSupportedBySwivel } from '../../swivel/formDataUtils/convertToFormData';

const propTypes = {
canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
Expand All @@ -22,6 +24,19 @@ export default function ExploreActionButtons({
if (slice) {
return (
<div className="btn-group results" role="group">
{
queryResponse &&
isSupportedBySwivel(queryResponse.form_data) &&
<a
href={getSwivelUrl(queryResponse.form_data, true)}
className="btn btn-default btn-sm"
title="Open in Swivel"
target="_blank"
rel="noopener noreferrer"
>
Open in Swivel
</a>
}
<URLShortLinkButton slice={slice} />

<EmbedCodeButton slice={slice} />
Expand Down
34 changes: 31 additions & 3 deletions superset/assets/javascripts/explore/exploreUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
/* eslint camelcase: 0 */
import URI from 'urijs';
import { compressToBase64 } from 'lz-string';

export function trimFormData(formData) {
const cleaned = { ...formData };
Object.entries(formData).forEach(([k, v]) => {
if (v === null || v === undefined) {
delete cleaned[k];
}
});
return cleaned;
}

export function getChartKey(explore) {
const slice = explore.slice;
Expand All @@ -14,8 +25,7 @@ export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
const endpoint = isNative ? 'annotation_json' : 'slice_json';
return uri.pathname(`/superset/${endpoint}/${slice_id}`)
.search({
form_data: JSON.stringify(form_data,
(key, value) => value === null ? undefined : value),
form_data: JSON.stringify(trimFormData(form_data)),
}).toString();
}

Expand Down Expand Up @@ -43,7 +53,7 @@ export function getExploreUrl(form_data, endpointType = 'base', force = false,

// Building the querystring (search) part of the URI
const search = uri.search(true);
search.form_data = JSON.stringify(form_data);
search.form_data = JSON.stringify(trimFormData(form_data));
if (force) {
search.force = 'true';
}
Expand All @@ -67,3 +77,21 @@ export function getExploreUrl(form_data, endpointType = 'base', force = false,
uri = uri.search(search).directory(directory);
return uri.toString();
}

export function getSwivelUrl(formData, lzCompress) {
if (!formData || !formData.datasource) {
return null;
}

const uri = URI(window.location.search);

// Building the query
if (lzCompress) {
return uri.pathname('/swivel')
.search({ lz_form_data: compressToBase64(JSON.stringify(trimFormData(formData))) })
.toString();
}
return uri.pathname('/swivel')
.search({ form_data: JSON.stringify(trimFormData(formData)) })
.toString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ export default function exploreReducer(state = {}, action) {
}
return state;
}

5 changes: 5 additions & 0 deletions superset/assets/javascripts/swivel/ColumnTypes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
TIMESTAMP: 'TIMESTAMP',
NUMERIC: 'NUMERIC',
STRING: 'NVARCHAR',
};
4 changes: 4 additions & 0 deletions superset/assets/javascripts/swivel/ContainerTypes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
SPLIT: 'SPLIT',
FILTER: 'FILTER',
};
5 changes: 5 additions & 0 deletions superset/assets/javascripts/swivel/FilterTypes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
INTERVAL: 'INTERVAL',
SELECT: 'SELECT',
UNBOUND: 'UNBOUND',
};
3 changes: 3 additions & 0 deletions superset/assets/javascripts/swivel/ItemTypes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
DIMENSION: 'dimension',
};
98 changes: 98 additions & 0 deletions superset/assets/javascripts/swivel/SessionManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import moment from 'moment';
import uuidv4 from 'uuid/v4';

import { LOCAL_STORAGE_SESSIONS_KEY,
LOCAL_STORAGE_KEY_PREFIX, MAX_NUM_SESSIONS } from './constants';

export function getSessions() {
let sessions = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SESSIONS_KEY));
if (!Array.isArray(sessions)) {
sessions = [];
} else {
sessions = sessions.filter(x => !!x && !!x.ts).sort((a, b) => b.ts - a.ts);
}
return sessions;
}

function saveSessions(sessions) {
localStorage.setItem(LOCAL_STORAGE_SESSIONS_KEY, JSON.stringify(sessions));
return sessions;
}

function cleanup(sessions) {
// Cleanup old sessions
Object.keys(localStorage)
.filter(x => x.startsWith(LOCAL_STORAGE_KEY_PREFIX) &&
!sessions.find(s => `${LOCAL_STORAGE_KEY_PREFIX}${s.id}` === x))
.forEach(x => localStorage.removeItem(x));
saveSessions(sessions);
}

export function deleteSessions() {
cleanup([]);
}

export function deleteSession(id) {
const sessions = getSessions();
cleanup(sessions.filter(x => x.id !== id));
}

export function createNewSession(name, id) {
let sessions = getSessions();
if (sessions.length > MAX_NUM_SESSIONS) {
localStorage.removeItem(sessions[sessions.length - 1]);
sessions.pop();
}
const newSession = { ts: moment.now(), id: id || uuidv4(), name };
sessions = [newSession, ...sessions];
cleanup(sessions);
return newSession;
}

export function updateSession(id, name) {
const sessions = getSessions();
let session = sessions.find(x => x.id === id);
if (session) {
session.name = name || session.name;
session.ts = moment.now();
saveSessions(sessions);
} else {
session = createNewSession(name, id);
}
window.document.title = `Swivel - ${session.name} (${session.id.substring(0, 7)})`;
}

export function getSessionKey(bootstrapData) {
let swivelSession = null;
const sessions = getSessions();

const createNew = bootstrapData.new ||
bootstrapData.reset ||
bootstrapData.lz_form_data ||
bootstrapData.form_data;

const now = moment.now();
// Read the current session from Local Storage
if (bootstrapData.session &&
sessions.find(x => x.id === bootstrapData.session)) {
// Session was passed in with bootstrapData
const s = sessions.find(x => x.id === bootstrapData.session);
s.ts = now;
swivelSession = s.id;
} else if (sessions.length && !createNew) {
// Get the most recent session.
const s = sessions[0];
swivelSession = s.id;
s.ts = now;
}

// Create a new Session
if (!swivelSession || createNew) {
swivelSession = createNewSession('').id;
} else {
saveSessions(sessions);
}

window.history.pushState('', '', `${location.pathname}?session=${swivelSession}`);
return swivelSession;
}
51 changes: 51 additions & 0 deletions superset/assets/javascripts/swivel/actions/globalActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Aborts the current query
export const ABORT = 'ABORT';
export function abort() {
return { type: ABORT };
}

export const RESET = 'RESET';
export const CLEAR_HISTORY = 'CLEAR_HISTORY';
export function reset(clearHistory) {
if (clearHistory) {
return dispatch =>
// We need the sandwich to make sure there is enough space in the
// local storage to RESET
Promise.resolve(dispatch({ type: ABORT }))
.then(() => dispatch({ type: CLEAR_HISTORY }))
.then(() => dispatch({ type: RESET }))
.then(() => dispatch({ type: CLEAR_HISTORY }));
}
return dispatch =>
Promise.resolve(dispatch({ type: ABORT }))
.then(() => dispatch({ type: RESET }));
}

// This controls whether a query should be run
export const SET_RUN = 'SET_RUN';
export function setRun(run) {
return { type: SET_RUN, run };
}

// This controls if a query should automatically run if the query settings change
export const SET_AUTO_RUN = 'SET_AUTO_RUN';
export function setAutoRun(autoRun) {
return { type: SET_AUTO_RUN, autoRun };
}

// This indicates if a query is currently running
export const SET_IS_RUNNING = 'SET_IS_RUNNING';
export function setIsRunning(isRunning, queryRequest) {
return { type: SET_IS_RUNNING, isRunning, queryRequest };
}

export const SET_ERROR = 'SET_ERROR';
export function setError(error) {
return { type: SET_ERROR, error };
}

export const UPDATE_FORM_DATA = 'UPDATE_FORM_DATA';
export const IMPORT_FORM_DATA = 'IMPORT_FORM_DATA';
export function importFormData(formData, refData) {
return { type: IMPORT_FORM_DATA, formData, refData };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const SEARCH_COLUMNS = 'SEARCH_COLUMNS';
export function searchColumns() {
return { type: SEARCH_COLUMNS };
}

export const SEARCH_METRICS = 'SEARCH_METRICS';
export function searchMetrics() {
return { type: SEARCH_METRICS };
}
105 changes: 105 additions & 0 deletions superset/assets/javascripts/swivel/actions/querySettingsActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { convertQuerySettingsToFormData } from '../formDataUtils/convertToFormData';

import { fetchDatasources, fetchDatasourceMetadata } from './refDataActions';
import { UPDATE_FORM_DATA, setError, setAutoRun, importFormData } from './globalActions';
import { runQuery } from './vizDataActions';

export const SET_DEFAULTS = 'SET_DEFAULTS';
export function setDefaults(refData) {
return { type: SET_DEFAULTS, refData };
}

export const SET_DATASOURCE = 'SET_DATASOURCE';
export function setDatasource(uid, init = true) {
return (dispatch, getState) => {
if (getState().settings.future.length === 0 &&
getState().settings.present.query.datasource === uid &&
getState().refData.columns.length) {
return Promise.resolve();
}
return dispatch(fetchDatasourceMetadata(uid))
.then(() => dispatch({
type: SET_DATASOURCE,
uid,
name: (getState()
.refData
.datasources.find(x => x.uid === uid) || {}).name,
}))
.then(() => init ? dispatch(
setDefaults(getState().refData)) : Promise.resolve());
};
}

export const BOOTSTRAP = 'BOOTSTRAP';
export function bootstrap(formData) {
return (dispatch, getState) =>
dispatch(fetchDatasources()).then(() => {
const datasource = getState().settings.present.query.datasource;
if (formData.datasource) {
return Promise.resolve(dispatch(setAutoRun(false)))
.then(() => dispatch(setDatasource(formData.datasource, false)))
.then(() => dispatch(importFormData(formData, getState().refData)))
.then(() => dispatch(setAutoRun(true)));
} else if (datasource) {
return dispatch(setDatasource(datasource, false));
}
return Promise.resolve();
});
}

export function updateFormDataAndRunQuery(settings) {
return (dispatch) => {
const formData = convertQuerySettingsToFormData(settings);
return Promise.resolve(
dispatch({ type: UPDATE_FORM_DATA, formData, wipeData: true }))
.then(() => dispatch(setError(formData.error)))
.then(() => dispatch(runQuery()));
};
}

export const TOGGLE_METRIC = 'TOGGLE_METRIC';
export function toggleMetric(metric) {
return { type: TOGGLE_METRIC, metric };
}

export const ADD_FILTER = 'ADD_FILTER';
export function addFilter(filter) {
return { type: ADD_FILTER, filter };
}

export const CONFIGURE_FILTER = 'CONFIGURE_FILTER';
export function configureFilter(filter) {
return { type: CONFIGURE_FILTER, filter };
}

export const REMOVE_FILTER = 'REMOVE_FILTER';
export function removeFilter(filter) {
return { type: REMOVE_FILTER, filter };
}

export const ADD_SPLIT = 'ADD_SPLIT';
export function addSplit(split) {
return { type: ADD_SPLIT, split };
}

export const CONFIGURE_SPLIT = 'CONFIGURE_SPLIT';
export function configureSplit(split) {
return { type: CONFIGURE_SPLIT, split };
}

export const REMOVE_SPLIT = 'REMOVE_SPLIT';
export function removeSplit(split) {
return { type: REMOVE_SPLIT, split };
}

export const CHANGE_INTERVAL = 'CHANGE_INTERVAL';
export function changeInterval(intervalStart, intervalEnd) {
return { type: CHANGE_INTERVAL, intervalStart, intervalEnd };
}

// TODO need to move those to the vizSettings
export const SET_VIZTYPE = 'SET_VIZTYPE';
export function setVizType(vizType) {
return { type: SET_VIZTYPE, vizType };
}

Loading