From 265e117a4a95136f71a7806903de961fe507930e Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Thu, 23 May 2019 11:22:15 -0700 Subject: [PATCH] Add link to scheduled pipeline (#7584) * Add link to scheduled pipeline * Split utils into separate file * Fix unit test * Fix separator recursion --- docs/installation.rst | 6 ++ .../javascripts/showSavedQuery/utils_spec.jsx | 65 +++++++++++++++++++ superset/assets/src/showSavedQuery/index.css | 20 ++++++ superset/assets/src/showSavedQuery/index.jsx | 38 +++++++---- superset/assets/src/showSavedQuery/utils.js | 44 +++++++++++++ superset/views/sql_lab.py | 5 +- 6 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 superset/assets/spec/javascripts/showSavedQuery/utils_spec.jsx create mode 100644 superset/assets/src/showSavedQuery/index.css create mode 100644 superset/assets/src/showSavedQuery/utils.js diff --git a/docs/installation.rst b/docs/installation.rst index 329debc8a..3e2934a37 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -911,6 +911,12 @@ To allow scheduled queries, add the following to your `config.py`: 'container': 'end_date', }, ], + # link to the scheduler; this example links to an Airflow pipeline + # that uses the query id and the output table as its name + 'linkback': ( + 'https://airflow.example.com/admin/airflow/tree?' + 'dag_id=query_${id}_${extra_json.schedule_info.output_table}' + ), }, } diff --git a/superset/assets/spec/javascripts/showSavedQuery/utils_spec.jsx b/superset/assets/spec/javascripts/showSavedQuery/utils_spec.jsx new file mode 100644 index 000000000..d198a4918 --- /dev/null +++ b/superset/assets/spec/javascripts/showSavedQuery/utils_spec.jsx @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { getNestedValue, interpolate } from '../../../src/showSavedQuery/utils'; + +describe('getNestedValue', () => { + it('is a function', () => { + expect(typeof getNestedValue).toBe('function'); + }); + + it('works with simple ids', () => { + const obj = { a: '1' }; + const id = 'a'; + expect(getNestedValue(obj, id)).toEqual('1'); + }); + + it('works with complex ids', () => { + const obj = { a: { b: '1' } }; + const id = 'a.b'; + expect(getNestedValue(obj, id)).toEqual('1'); + }); + + it('works with other separators', () => { + const obj = { a: { b: { c: '1' } } }; + const id = 'a__b__c'; + const separator = '__'; + expect(getNestedValue(obj, id, separator)).toEqual('1'); + }); +}); + + +describe('interpolate', () => { + it('is a function', () => { + expect(typeof interpolate).toBe('function'); + }); + + it('works with simple ids', () => { + const obj = { a: '1' }; + // eslint-disable-next-line no-template-curly-in-string + const str = 'value: ${a}'; + expect(interpolate(str, obj)).toEqual('value: 1'); + }); + + it('works with complex ids', () => { + const obj = { a: { b: '1' } }; + // eslint-disable-next-line no-template-curly-in-string + const str = 'value: ${a.b}'; + expect(interpolate(str, obj)).toEqual('value: 1'); + }); +}); diff --git a/superset/assets/src/showSavedQuery/index.css b/superset/assets/src/showSavedQuery/index.css new file mode 100644 index 000000000..026dd784f --- /dev/null +++ b/superset/assets/src/showSavedQuery/index.css @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +.btn-add { display: none; } +.linkback { padding: 0 10px 20px 2px; } diff --git a/superset/assets/src/showSavedQuery/index.jsx b/superset/assets/src/showSavedQuery/index.jsx index cd384fe5b..c64626cb6 100644 --- a/superset/assets/src/showSavedQuery/index.jsx +++ b/superset/assets/src/showSavedQuery/index.jsx @@ -19,25 +19,39 @@ import React from 'react'; import ReactDom from 'react-dom'; import Form from 'react-jsonschema-form'; +import { interpolate } from 'src/showSavedQuery/utils'; +import './index.css'; const scheduleInfoContainer = document.getElementById('schedule-info'); const bootstrapData = JSON.parse(scheduleInfoContainer.getAttribute('data-bootstrap')); -const schemas = bootstrapData.common.feature_flags.SCHEDULED_QUERIES; -const scheduleInfo = bootstrapData.common.extra_json.schedule_info; +const config = bootstrapData.common.feature_flags.SCHEDULED_QUERIES; +const query = bootstrapData.common.query; +const scheduleInfo = query.extra_json.schedule_info; +const linkback = config.linkback + ? interpolate(config.linkback, query) + : null; -if (scheduleInfo && schemas) { +if (scheduleInfo && config) { // hide instructions when showing schedule info - schemas.JSONSCHEMA.description = ''; + config.JSONSCHEMA.description = ''; ReactDom.render( -
-
-
, +
+
+
+
+ {linkback &&
+ +   + Pipeline status + +
} +
, scheduleInfoContainer, ); } diff --git a/superset/assets/src/showSavedQuery/utils.js b/superset/assets/src/showSavedQuery/utils.js new file mode 100644 index 000000000..9cd712bc8 --- /dev/null +++ b/superset/assets/src/showSavedQuery/utils.js @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function getNestedValue(obj, id, separator = '.') { + /* + * Given a nested object and an id, return the nested value. + * + * > getNestedValue({a:{b:1}}, 'a.b') + * < 1 + */ + const index = id.indexOf(separator); + if (index === -1) { + return obj[id]; + } + const name = id.slice(0, index); + const rest = id.slice(index + separator.length); + return getNestedValue(obj[name], rest, separator); +} + +export function interpolate(str, obj) { + /* + * Programmatic template string for interpolation. + * + * > interpolate('foo ${a.b}', {a:{b:1}}) + * < "foo 1" + */ + return str.replace(/\$\{(.+?)\}/g, (match, id) => getNestedValue(obj, id)); +} diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index 37c84c270..98046f891 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -122,11 +122,12 @@ class SavedQueryView(SupersetModelView, DeleteMixin): def show(self, pk): pk = self._deserialize_pk_if_composite(pk) widgets = self._show(pk) - extra_json = self.datamodel.get(pk).extra_json + query = self.datamodel.get(pk).to_json() + query['extra_json'] = json.loads(query['extra_json']) payload = { 'common': { 'feature_flags': get_feature_flags(), - 'extra_json': json.loads(extra_json), + 'query': query, }, }