def header(column: Column): if column.sortable(): if query.sort_column_name == column.column_name and query.sort_order == 'ASC': icon = _.span(class_=('fa fa-sort-amount-asc'))[''] elif query.sort_column_name == column.column_name and query.sort_order == 'DESC': icon = _.span(class_=('fa fa-sort-amount-desc'))[''] else: icon = '' return _.a(href="#", name=flask.escape(column.column_name))[ icon, ' ', flask.escape(column.column_name)] else: return flask.escape(column.column_name)
def index_page(): """Overview page of mara_db""" return response.Response( title=f'Database schemas', html=bootstrap.card(body=[ _.div( style= 'display:inline-block; margin-top:15px; margin-bottom:15px; margin-right:50px;' )[_.a(href=flask.url_for('mara_db.schema_page', db_alias=db_alias) )[_.span(class_='fa fa-database')[''], ' ', db_alias], _.br, _.span(style='color:#888')[escape(str(type(db).__name__))]] for db_alias, db in config.databases().items() ]), js_files=[flask.url_for('mara_db.static', filename='schema-page.js')])
def _render_preview_row(query, row): values = [] for pos, value in enumerate(row): if value == '🔒': values.append(acl.inline_permission_denied_message('Restricted personal data')) elif query.column_names[pos] in query.data_set.custom_column_renderers: values.append(query.data_set.custom_column_renderers[query.column_names[pos]](value)) elif not value: values.append('') elif query.data_set.columns[query.column_names[pos]].type == 'text[]': values.append(_.ul[[_.li[_.span(class_='preview-value')[str(array_element)]] for array_element in value]]) elif query.data_set.columns[query.column_names[pos]].type == 'json': values.append(_.pre(class_='preview-value')[flask.escape(json.dumps(value, indent=2))]) else: values.append(_.span(class_='preview-value')[str(value)]) return _.tr[[_.td[value] for value in values]]
def document(doc_id, folder_id=""): docs = all_docs() if folder_id: full_doc_id = folder_id + '/' + doc_id else: full_doc_id = doc_id if full_doc_id not in docs: raise flask.abort(404, f"Documentation {doc_id} is not known.") doc = docs[full_doc_id] if not doc.path.exists(): raise flask.abort(404, f"Documentation {doc_id} is not found ({doc.path}).") with doc.path.open() as f: md_content = f.read() md_escaped = flask.escape(md_content) return response.Response( title=f'Doc "{doc.full_name}"', html=[bootstrap.card( body=[ _.div(style="display:none")[_.pre(id_='markdown-source')[md_escaped]], _.div(id_='markdown-rendered-content')[ _.span(class_='fa fa-spinner fa-spin')[' '] ], ]), _.script(type="text/javascript")[__render_code], ], js_files=[ 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.7.0/mermaid.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/markdown-it/11.0.0/markdown-it.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js', flask.url_for('docs.static', filename='markdown-it-naive-mermaid.js'), ], css_files=[ 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/default.min.css', ] )
def render_entry(entry: navigation.NavigationEntry, level: int = 1): attrs = {} if entry.children: attrs.update({'onClick': 'toggleNavigationEntry(this)'}) else: attrs.update({ 'onClick': 'highlightNavigationEntry(\'' + entry.uri_fn() + '\');collapseNavigation()', 'href': entry.uri_fn() }) if entry.description: attrs.update({ 'title': entry.description, 'data-toggle': 'tooltip', 'data-container': 'body', 'data-placement': 'right' }) return _.div( _class='mara-nav-entry level-' + str(level), style='display:none' if level > 1 else '')[_.a(**attrs)[_.span(_class='fa fa-fw fa-' + entry.icon + ( ' fa-lg' if level == 1 else ''))[''] if entry.icon else '', entry.label.replace('_', '_<wbr>'), _.div(_class='mara-caret fa fa-caret-down' )[''] if entry.children else ''], render_entries(entry.children, level + 1)]
def action_button(button: mara_page.response.ActionButton): """Renders an action button""" return [ _.a(_class='btn', href=button.action, title=button.title)[_.span(_class='fa fa-' + button.icon)[''], ' ', button.label] ]
def page_header(response: mara_page.response.Response): """Renders the fixed top part of the page""" return _.nav(id='mara-page-header', _class='navbar fixed-top')[ _.a(_class='navigation-toggle-button fa fa-lg fa-reorder', onclick='toggleNavigation()')[' '], _.h1()[response.title], _.img(src=config.logo_url()), _.span(_class='action-buttons')[map(action_button, response. action_buttons)]]
def spinner() -> [str]: """ Returns markup for an animated load spinner. The default version requires font-awesome to be installed Returns: html markup """ return _.span(class_='fa fa-spinner fa-spin')[' ']
def button(url: str, label: str, title: str, icon: str, id: str = None): """ Renders a bootstrap button Args: url: The action to perform label: The button label title: A help message icon: An icon from the `fontawesome`_ collection id: An id that is added to the element Returns: The rendered button .. _fontawesome: http://fontawesome.io/icons/ """ return _.a(class_='btn mara-button', href=url, title=title, id=id or uuid.uuid1())[ _.span(class_='fa fa-' + icon)[''], ' ', label]
def data_set_page(data_set_id, query_id): from .data_set import find_data_set ds = find_data_set(data_set_id) if not ds: flask.flash(f'Data set "{data_set_id}" does not exist anymore', category='danger') return flask.redirect(flask.url_for('mara_data_explorer.index_page')) action_buttons = [] action_buttons.append(response.ActionButton(action='javascript:dataSetPage.downloadCSV()', icon='download', label='CSV', title='Download as CSV')) if config.google_sheet_oauth2_client_config(): action_buttons.append(response.ActionButton(action='javascript:dataSetPage.exportToGoogleSheet()', icon='cloud-upload', label='Google sheet', title='Export to a Google sheet')) action_buttons.append(response.ActionButton(action='javascript:dataSetPage.load()', icon='folder-open', label='Load', title='Load previously saved query')) action_buttons.append(response.ActionButton(action='javascript:dataSetPage.save()', icon='save', label='Save', title='Save query')) action_buttons.append(response.ActionButton(action='javascript:dataSetPage.displayQuery()', icon='eye', label='SQL', title='Display query')) if query_id: action_buttons.insert(1, response.ActionButton( action=flask.url_for('mara_data_explorer._delete_query', data_set_id=data_set_id, query_id=query_id), icon='trash', label='Delete', title='Delete query')) return response.Response( title=f'Query "{query_id}" on "{ds.name}"' if query_id else f'New query on "{ds.name}"', html=[_.div(class_='row')[ _.div(class_='col-md-3')[ bootstrap.card(header_left='Query', body=_.div(id='query-details')[html.spinner()]), bootstrap.card(header_left='Columns', header_right=_.a(id='select-all', href='#')[' Select all'], body=[_.div(class_="form-group")[ _.input(type="search", class_="columns-search form-control", value="", placeholder="Filter")], _.div(id='columns-list')[html.spinner()]])], _.div(class_='col-md-9')[ bootstrap.card( id='filter-card', header_left=[_.div(class_="dropdown")[ _.a(**{'class': 'dropdown-toggle', 'data-toggle': 'dropdown', 'href': '#'})[ _.span(class_='fa fa-plus')[' '], ' Add filter'], _.div(class_="dropdown-menu", id='filter-menu')[ _.div(class_="dropdown-item")[ _.input(type="text", class_="columns-search form-control", value="", placeholder="Filter")]]]], fixed_header_height=False, body=_.div(id='filters')[html.spinner()]), bootstrap.card(header_left=_.div(id='row-counts')[html.spinner()], header_right=_.div(id='pagination')[html.spinner()], body=_.div(id='preview')[html.spinner()]), _.div(class_='row', id='distribution-charts')[''] ]], _.script[f""" var dataSetPage = null; document.addEventListener('DOMContentLoaded', function() {{ dataSetPage = DataSetPage('{flask.url_for('mara_data_explorer.index_page')}', {json.dumps( {'data_set_id': data_set_id, 'query_id': query_id, 'query': flask.request.get_json()})}, 15, '{config.charts_color()}'); }}); """], html.spinner_js_function(), _.div(class_='col-xl-4 col-lg-6', id='distribution-chart-template', style='display: none')[ bootstrap.card(header_left=html.spinner(), body=_.div(class_='chart-container google-chart')[ html.spinner()])], _.div(class_='modal fade', id='load-query-dialog', tabindex="-1")[ _.div(class_='modal-dialog', role='document')[ _.div(class_='modal-content')[ _.div(class_='modal-header')[ _.h5(class_='modal-title')['Load query'], _.button(**{'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close"})[ _.span(**{'aria-hidden': 'true'})['×']]], _.div(class_='modal-body', id='query-list')[''] ] ] ], _.div(class_='modal fade', id='display-query-dialog', tabindex="-1")[ _.div(class_='modal-dialog', role='document')[ _.div(class_='modal-content')[ _.div(class_='modal-header')[ _.h5(class_='modal-title')['Query statement'], _.button(**{'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close"})[ _.span(**{'aria-hidden': 'true'})['×']]], _.div(class_='modal-body', id='query-display')[''] ] ] ], _.form(action=flask.url_for('mara_data_explorer.download_csv', data_set_id=data_set_id), method='post')[ _.div(class_="modal fade", id="download-csv-dialog", tabindex="-1")[ _.div(class_="modal-dialog", role='document')[ _.div(class_="modal-content")[ _.div(class_="modal-header")[ _.h5(class_='modal-title')['Download as CSV'], _.button(**{'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close"})[ _.span(**{'aria-hidden': 'true'})['×']]], _.div(class_="modal-body")[ 'Delimiter:  ', _.input(type="radio", value="\t", name="delimiter", checked="checked"), ' tab   ', _.input(type="radio", value=";", name="delimiter"), ' semicolon   ', _.input(type="radio", value=",", name="delimiter"), ' comma   ', _.hr, 'Number format:  ', _.input(type="radio", value=".", name="decimal-mark", checked="checked"), ' 42.7   ', _.input(type="radio", value=",", name="decimal-mark"), ' 42,7   ', _.input(type="hidden", name="query")], _.div(class_="modal-footer")[ _.button(id="csv-download-button", type="submit", class_="btn btn-primary")[ 'Download']]]]]], _.form(action=flask.url_for('mara_data_explorer.oauth2_export_to_google_sheet', data_set_id=data_set_id), method='post', target="_blank")[ _.div(class_="modal fade", id="google-sheet-export-dialog", tabindex="-1")[ _.div(class_="modal-dialog", role='document')[ _.div(class_="modal-content")[ _.div(class_="modal-header")[ _.h5(class_='modal-title')['Google sheet export'], _.button(**{'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close"})[ _.span(**{'aria-hidden': 'true'})['×']]], _.div(class_="modal-body")[ 'Number format:  ', _.input(type="radio", value=".", name="decimal-mark", checked="checked"), ' 42.7   ', _.input(type="radio", value=",", name="decimal-mark"), ' 42,7   ', _.hr, 'Array format:  ', _.input(type="radio", value="curly", name="array-format", checked="checked"), ' {"a", "b"}   ', _.input(type="radio", value="normal", name="array-format"), ' ["a", "b"]   ', _.input(type="radio", value="tuple", name="array-format"), ' ("a", "b")   ', _.hr, 'By clicking Export below:', _.br, _.ul[ _.li['Google authentication will be required.'], _.li['A maximum limit of 100.000 rows will be applied.'], _.li['A maximum limit of 50.000 characters per cell will be applied.'], _.li['A Google sheet with the selected data will be available in a new tab.'] ], _.input(type="hidden", name="query") ], _.div(class_="modal-footer")[ _.button(id="export-to-google-sheet", type="submit", class_="btn btn-primary")[ 'Export']]]]]] ], action_buttons=action_buttons, js_files=['https://www.gstatic.com/charts/loader.js', flask.url_for('mara_data_explorer.static', filename='tagsinput.js'), flask.url_for('mara_data_explorer.static', filename='typeahead.js'), flask.url_for('mara_data_explorer.static', filename='data-sets.js')], css_files=[flask.url_for('mara_data_explorer.static', filename='tagsinput.css'), flask.url_for('mara_data_explorer.static', filename='data-sets.css')])
def start_page(): import mara_pipelines.config from mara_data_explorer.data_set import find_data_set data_set_for_preview = find_data_set('order_items') assert (data_set_for_preview) return response.Response( title='MyCompany BI', html=_.div(class_='row')[_.div( class_='col-lg-6' )[bootstrap. card(header_left=_.b['Welcome'], body=[ _. p['This is the first thing that users of your data warehouse will see. ', 'Please add links to relevant documentation, tutorials & other ', 'data tools in your organization.'], _.p[ _.a(href='https://github.com/mara/mara-example-project-1/blob/master/app/ui/start_page.py' )[ 'Here'], ' is the source code for this page, and here is a picture of a ', _.a(href='https://en.wikipedia.org/wiki/Mara_(mammal)' )[ 'mara'], ':'], _. img(src=flask.url_for('ui.static', filename='mara.jpg'), style ='width:40%; margin-left: auto; margin-right:auto; display:block;' ) ]), bootstrap.card(header_left=[ _.b[_.a(href=flask.url_for('mara_metabase.metabase'))[_.span( class_='fa fa-bar-chart')[''], ' Metabase']], ' & ', _. b[_.a(href=flask.url_for('mara_mondrian.saiku'))[_.span( class_='fa fa-bar-chart')[''], ' Saiku']], ': Company wide dashboards, pivoting & ad hoc analysis' ], body=[ _. p['Metabase tutorial: ', _. a(href= 'https://www.metabase.com/docs/latest/getting-started.html' ) ['https://www.metabase.com/docs/latest/getting-started.html']], _. p['Saiku introduction: ', _. a(href= 'https://saiku-documentation.readthedocs.io/en/latest/' ) ['https://saiku-documentation.readthedocs.io/en/latest/']] ]), bootstrap.card(header_left=[ _.b[_.a(href=flask.url_for('mara_data_explorer.index_page'))[ _.span(class_='fa fa-table')[''], ' Explore']], ': Raw data access & segmentation' ], body=[ _.p[_.a( href=flask. url_for('mara_data_explorer.data_set_page', data_set_id=data_set_for_preview.id ))[data_set_for_preview.name], ':', html.asynchronous_content( flask.url_for( 'mara_data_explorer.data_set_preview', data_set_id=data_set_for_preview.id) )], _. p['Other data sets: ', ', '.join([ str( _.a(href=flask.url_for( 'mara_data_explorer.data_set_page', data_set_id=ds.id))[ds.name]) for ds in mara_data_explorer.config.data_sets( ) if ds.id != data_set_for_preview.id ])] ])], _.div(class_='col-lg-6') [bootstrap. card(header_left=[ _.b[_.a(href=flask.url_for( 'mara_schema.index_page'))[_.span( class_='fa fa-book' )[''], ' Data sets']], ': Documentation of attributes and metrics of all data sets' ], body=html.asynchronous_content( url=flask. url_for('mara_schema.overview_graph' ))), bootstrap.card(header_left=[ _.b[_.a( href=flask. url_for('mara_pipelines.node_page') )[_.span(class_='fa fa-wrench')[''], ' Pipelines']], ': The data integration pipelines that create the DWH' ], body=html. asynchronous_content( flask.url_for( 'mara_pipelines.dependency_graph', path='/'))), bootstrap.card(header_left=[ _.b[_.a(href=flask. url_for('mara_db.index_page') )[_.span( class_='fa fa-database')[''], ' Database Schemas']], ': Schemas of all databases connections' ], body=[ html.asynchronous_content( flask.url_for( 'mara_db.draw_schema', db_alias= mara_pipelines. config. default_db_alias( ), schemas='ec_dim' ) + '?hide-columns=True') ])]])
def view_plots(): return response.Response( html=[ _.div(id='forecast-container')[_.div(class_="")[[ [ _.div(class_='row')[_.div(class_='col-xl-12')[_.div( class_='section-header')[forecast.metric_name]]], _.div(class_='row')[_.div(class_='col-xl-12')[ _.p()['Number of days: ' + str(forecast.number_of_days), _.br(), 'Time-series query: ', html.highlight_syntax(forecast.time_series_query, language='postgresql')], bootstrap.card( header_left='Forecast plot of "' + forecast.metric_name + '"', header_right=_.div()[ # config.forecast_table_name # _.a(class_='query-control', style='margin-right: 10px', # href='#')[ # _.span(class_='fa fa-download')[' ']], _. a(class_='query-control', href=f"javascript:showQueryDetails('" f"{flask.url_for('forecasts.query_details', name=forecast.metric_name)}')" )[_.span(class_='fa fa-eye')[' ']]], body=[ html.asynchronous_content( flask.url_for('forecasts.get_plot_image', name=forecast.metric_name. lower().replace(' ', '_'). replace('-', '_'), components='False')), _.br(), _.br(), _.div(class_='modal fade', id='query-details-dialog', tabindex="-1" )[_.div(class_='modal-dialog modal-lg', role='document')[_.div( class_='modal-content' )[_.div( class_='modal-header' )[_.h5(class_='modal-title' )['Time-series query'], _.button( **{ 'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close" } )[_.span( **{'aria-hidden': 'true'} )['×']]], _.div(class_='modal-body', id='query-details' )['']]]], ])], _. div(class_='col-xl-12')[bootstrap.card( header_left= 'Forecast components (trend, holidays, seasonality) of "{}"' .format(forecast.metric_name), header_right='', body=[ html.asynchronous_content( flask.url_for( 'forecasts.get_plot_image', name=forecast. metric_name.lower( ).replace(' ', '_'). replace('-', '_'), components='True')), ])], _.hr()] ] for forecast in config.forecasts() ]]], _.script[''''''] ], title='Forecasts', css_files=[ 'https://fonts.googleapis.com/css?family=Open+Sans:300,400,700', flask.url_for('forecasts.static', filename='forecast.css') ], js_files=[ flask.url_for('forecasts.static', filename='forecast.js'), 'https://www.gstatic.com/charts/loader.js' ])
def data_set_page(data_set_id, query_id): ds = find_data_set(data_set_id) if not ds: flask.flash(f'Data set "{data_set_id}" does not exist anymore', category='danger') return flask.redirect(flask.url_for('data_sets.index_page')) action_buttons = [ response.ActionButton(action='javascript:dataSetPage.downloadCSV()', icon='download', label='CSV', title='Download as CSV'), response.ActionButton(action='javascript:dataSetPage.load()', icon='folder-open', label='Load', title='Load previously saved query'), response.ActionButton(action='javascript:dataSetPage.save()', icon='save', label='Save', title='Save query') ] if query_id: action_buttons.insert( 1, response.ActionButton(action=flask.url_for( 'data_sets._delete_query', data_set_id=data_set_id, query_id=query_id), icon='trash', label='Delete', title='Delete query')) return response.Response( title=f'Query "{query_id}" on "{ds.name}"' if query_id else f'New query on "{ds.name}"', html=[ _.div( class_='row')[_.div( class_='col-md-3' )[bootstrap.card(header_left='Query', body=_.div( id='query-details')[html.spinner()]), bootstrap. card(header_left='Columns', body=[ _.div( class_="form-group" )[_.input(type="search", class_="columns-search form-control", value="", placeholder="Filter")], _.div(id='columns-list')[html.spinner()] ])], _.div(class_='col-md-9')[bootstrap.card( id='filter-card', header_left=[ _.div(class_="dropdown")[_.a( **{ 'class': 'dropdown-toggle', 'data-toggle': 'dropdown', 'href': '#' } )[_.span( class_='fa fa-plus')[' '], ' Add filter'], _. div(class_= "dropdown-menu", id= 'filter-menu' ) [_.div( class_ ="dropdown-item" )[_.input( type ="text", class_= "columns-search form-control", value="", placeholder= "Filter")]]] ], fixed_header_height=False, body=_.div(id='filters')[html.spinner()]), bootstrap. card(header_left=_.div( id='row-counts' )[html.spinner()], header_right=_.div( id='pagination' )[html.spinner()], body=_.div( id='preview' )[html.spinner()]), _. div(class_='row', id= 'distribution-charts' )['']]], _.script[f""" var dataSetPage = null; document.addEventListener('DOMContentLoaded', function() {{ dataSetPage = DataSetPage('{flask.url_for('data_sets.index_page')}', {json.dumps({'data_set_id': data_set_id, 'query_id': query_id, 'query': flask.request.get_json()})}, 15, '{config.charts_color()}'); }}); """], html.spinner_js_function(), _.div(class_='col-xl-4 col-lg-6', id='distribution-chart-template', style='display: none')[bootstrap.card( header_left=html.spinner(), body=_.div(class_='chart-container google-chart')[ html.spinner()])], _.div( class_='modal fade', id='load-query-dialog', tabindex="-1")[_.div( class_='modal-dialog', role='document')[_.div( class_='modal-content')[_.div(class_='modal-header')[ _.h5(class_='modal-title')['Load query'], _.button( **{ 'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close" })[_.span( **{'aria-hidden': 'true'})['×']]], _.div(class_='modal-body', id='query-list')['']]]], _.form( action=flask.url_for('data_sets.download_csv'), method='post')[_.div( class_="modal fade", id="download-csv-dialog", tabindex="-1" )[_.div(class_="modal-dialog", role='document')[_.div( class_="modal-content" )[_.div(class_="modal-header") [_.h5(class_='modal-title')['Download as CSV'], _.button( **{ 'type': "button", 'class': "close", 'data-dismiss': "modal", 'aria-label': "Close" })[_.span( **{'aria-hidden': 'true'})['×']]], _.div(class_="modal-body")[ 'Delimiter:  ', _.input(type="radio", value="\t", name="delimiter", checked="checked"), ' tab   ', _.input(type="radio", value=";", name="delimiter"), ' semicolon   ', _.input(type="radio", value=",", name="delimiter"), ' comma   ', _.hr, 'Number format:  ', _.input(type="radio", value=".", name="decimal-mark", checked="checked"), ' 42.7   ', _.input(type="radio", value=",", name="decimal-mark"), ' 42,7   ', _.input(type="hidden", name="query")], _.div(class_="modal-footer" )[_.button(id="csv-download-button", type="submit", class_="btn btn-primary")['Download']]]]]] ], action_buttons=action_buttons, js_files=[ 'https://www.gstatic.com/charts/loader.js', flask.url_for('data_sets.static', filename='tagsinput.js'), flask.url_for('data_sets.static', filename='typeahead.js'), flask.url_for('data_sets.static', filename='data-sets.js') ], css_files=[ flask.url_for('data_sets.static', filename='tagsinput.css'), flask.url_for('data_sets.static', filename='data-sets.css') ])