Commit 756dd480 authored by Jacob Schatz's avatar Jacob Schatz

Add explore view.

parent 231fe0bf
......@@ -20,6 +20,8 @@ SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{user}:{pw}@{url}/{db}'.format(
SQLALCHEMY_TRACK_MODIFICATIONS = False
# SQLALCHEMY_ECHO=True
THREADS_PER_PAGE = 2
SECRET_KEY = "damnitjanice"
\ No newline at end of file
......@@ -103,8 +103,10 @@ def db_import():
new_view_settings['label'] = file_view['label']
if 'sql_table_name' in file_view:
new_view_settings['sql_table_name'] = file_view['sql_table_name']
new_view_settings = json.dumps(new_view_settings)
new_view = View(file_view['_view'], new_view_settings)
# Add dimensions for view
if 'dimensions' in file_view:
for dimension in file_view['dimensions']:
new_dimension_settings = {}
if 'hidden' in dimension:
......@@ -119,11 +121,31 @@ def db_import():
new_dimension_settings['sql'] = dimension['sql']
new_dimension_settings['_type'] = dimension['_type']
new_dimension_settings['_n'] = dimension['_n']
new_dimension_settings = json.dumps(new_dimension_settings)
new_dimension = Dimension(dimension['_dimension'], new_dimension_settings)
new_view.dimensions.append(new_dimension)
db.session.add(new_dimension)
new_view.dimensions.append(new_dimension)
# Add measures for view
if 'measures' in file_view:
for measure in file_view['measures']:
new_measure_settings = {}
if 'hidden' in measure:
new_measure_settings['hidden'] = measure['hidden']
if 'value_format' in measure:
new_measure_settings['value_format'] = measure['value_format']
if 'label' in measure:
new_measure_settings['label'] = measure['label']
if 'type' in measure:
new_measure_settings['type'] = measure['type']
if 'sql' in measure:
new_measure_settings['sql'] = measure['sql']
new_measure_settings['_type'] = measure['_type']
new_measure_settings['_n'] = measure['_n']
new_measure = Measure(measure['_measure'], new_measure_settings)
new_view.measures.append(new_measure)
db.session.add(new_measure)
new_view.measures.append(new_measure)
db.session.add(new_view)
# process models
for model in models:
......@@ -134,7 +156,6 @@ def db_import():
model_settings['connection'] = model['connection']
model_settings['_type'] = model['_type']
model_settings = json.dumps(model_settings)
new_model = Model(model['_model'], model_settings)
# Set the explores for the model
......@@ -149,7 +170,6 @@ def db_import():
explore_settings['description'] = explore['description']
explore_settings['_type'] = explore['_type']
explore_settings = json.dumps(explore_settings)
new_explore = Explore(explore['_explore'], explore_settings)
# Set the view for the explore
......@@ -163,6 +183,8 @@ def db_import():
explore_join_settings = {}
if 'view_label' in join:
explore_join_settings['view_label'] = join['view_label']
if 'label' in join:
explore_join_settings['label'] = join['label']
if 'type' in join:
explore_join_settings['type'] = join['type']
if 'relationship' in join:
......@@ -188,3 +210,51 @@ def db_import():
def db_test():
explore = Explore.query.first()
return jsonify({'explore': {'name': explore.name, 'settings': explore.settings}})
@bp.route('/models', methods=['GET'])
def models():
models = Model.query.all()
models_json = []
for model in models:
this_model = {}
this_model['settings'] = model.settings
this_model['name'] = model.name
this_model['explores'] = []
for explore in model.explores:
this_explore = {}
this_explore['settings'] = explore.settings
this_explore['name'] = explore.name
this_explore['link'] = '/explore/{}/{}'.format(model.name, explore.name)
this_explore['views'] = []
this_explore['joins'] = []
for view in explore.views:
this_view = {}
this_view['name'] = view.name
this_view['settings'] = view.settings
this_explore['views'].append(this_view)
for join in explore.joins:
this_join = {}
this_join['name'] = join.name
this_join['settings'] = join.settings
this_explore['joins'].append(this_join)
this_model['explores'].append(this_explore)
models_json.append(this_model)
return jsonify(models_json)
@bp.route('/explores', methods=['GET'])
def explores():
explores = Explore.query.all()
explores_json = []
for explore in explores:
explores_json.append(explore.serializable())
return jsonify(explores_json)
@bp.route('/explores/<model_name>/<explore_name>', methods=['GET'])
def explore_read(model_name, explore_name):
explore = Explore.query\
.join(Model, Explore.model_id == Model.id)\
.filter(Model.name == model_name)\
.filter(Explore.name == explore_name)\
.first()
return jsonify(explore.serializable(True))
\ No newline at end of file
from sqlalchemy.types import TypeDecorator, CHAR
from sqlalchemy.dialects.postgresql import UUID
import uuid
from app import db
class GUID(TypeDecorator):
"""Platform-independent GUID type.
Uses Postgresql's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
"""
impl = CHAR
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(UUID())
else:
return dialect.type_descriptor(CHAR(32))
def process_bind_param(self, value, dialect):
if value is None:
return value
elif dialect.name == 'postgresql':
return str(value)
else:
if not isinstance(value, uuid.UUID):
return "%.32x" % uuid.UUID(value).int
else:
# hexstring
return "%.32x" % value.int
def process_result_value(self, value, dialect):
if value is None:
return value
else:
return uuid.UUID(value)
class Base(db.Model):
__abstract__ = True
......
......@@ -48,6 +48,46 @@ class Explore(BaseLook):
def __repr__(self):
return '<Explore %i>' % (self.id)
def serializable(self, include_dimensions_and_measures=False):
this_explore = {}
this_explore['settings'] = self.settings
this_explore['name'] = self.name
this_explore_model = {}
this_explore_model['settings'] = self.model.settings
this_explore_model['name'] = self.model.name
this_explore['model'] = this_explore_model
this_explore['views'] = []
this_explore['joins'] = []
this_explore['unique_name'] = 'explore_{}'.format(self.name)
for view in self.views:
this_view = {}
this_view['name'] = view.name
this_view['settings'] = view.settings
this_view['unique_name'] = 'view_{}'.format(view.name)
if include_dimensions_and_measures:
this_view['dimensions'] = []
for dimension in view.dimensions:
this_dimension = {}
this_dimension['name'] = dimension.name
this_dimension['settings'] = dimension.settings
this_dimension['unique_name'] = 'dimension_{}'.format(dimension.name)
this_view['dimensions'].append(this_dimension)
this_view['measures'] = []
for measure in view.measures:
this_measure = {}
this_measure['name'] = measure.name
this_measure['settings'] = measure.settings
this_measure['unique_name'] = 'measure_{}'.format(measure.name)
this_view['measures'].append(this_measure)
this_explore['views'].append(this_view)
for join in self.joins:
this_join = {}
this_join['name'] = join.name
this_join['settings'] = join.settings
this_explore['joins'].append(this_join)
return this_explore
class Join(BaseLook):
__tablename__ = 'join'
......
......@@ -6,6 +6,7 @@ class Project(Base):
__tablename__ = 'project'
name = db.Column(db.String(128), nullable=False)
git_url = db.Column(db.String(), nullable=False)
validated = db.Column(db.Boolean(), default=False)
def __init__(self, name, git_url):
self.name = name
......
import axios from 'axios';
import utils from './utils';
export default {
index(model, explore) {
return axios.get(utils.buildUrl('repos/explores', `${model}/${explore}`));
},
};
......@@ -17,4 +17,8 @@ export default {
update() {
return axios.get(utils.buildUrl('repos', 'update'));
},
models() {
return axios.get(utils.buildUrl('repos', 'models'));
},
};
src/assets/logo.png

1.91 KB | W: | H:

src/assets/logo.png

4.13 KB | W: | H:

src/assets/logo.png
src/assets/logo.png
src/assets/logo.png
src/assets/logo.png
  • 2-up
  • Swipe
  • Onion skin
<template>
<nav class="level main-nav has-background-dark">
<p class="level-item has-text-centered">
<a href="#" class="link has-text-light">Home</a>
</p>
<p class="level-item has-text-centered">
<a href="#" class="link has-text-light">Browse</a>
</p>
<p class="level-item has-text-centered">
<img src="../assets/logo.png">
</p>
<p class="level-item has-text-centered">
<a href="#" class="link has-text-light">Explore</a>
</p>
<p class="level-item has-text-centered">
<a href="#" class="link has-text-light">Develop</a>
</p>
<nav class="navbar is-info">
<div class="navbar-brand">
<a class="navbar-item" href="#">
<img src="../assets/logo.png" alt="Melt: data analytics for all" width="112" height="28">
</a>
<div class="navbar-burger burger" data-target="meltnavbar-transparent">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="meltnavbar-transparent"
class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/">
Home
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">
Explore
</a>
<div class="navbar-dropdown
is-boxed"
:class="{'has-been-clicked': navbarClicked}">
<template v-for="model in models">
<div class="navbar-item navbar-title has-text-grey-light" :key="model.label">
{{model | printable | capitalize | underscoreToSpace}}
</div>
<router-link :to="explore.link"
class="navbar-item navbar-child"
v-for="explore in model.explores"
@click.native="menuSelected"
:key="explore.view_label">
{{explore.settings.label}}
</router-link>
</template>
</div>
</div>
<router-link to="/repo" class="navbar-item">{{project.name}}</router-link>
</div>
</div>
</nav>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'MainNav',
created() {
this.$store.dispatch('projects/getProjects');
this.$store.dispatch('repos/getModels');
},
filters: {
printable(value) {
return value.label ? value.label : value.name;
},
underscoreToSpace(value) {
return value.replace(/_/g, ' ');
},
},
computed: {
...mapState('projects', [
'project',
]),
...mapState('repos', [
'models',
'navbarClicked',
]),
},
methods: {
menuSelected() {
this.$store.dispatch('repos/navbarHideDropdown');
},
},
};
</script>
<style lang="scss">
.main-nav {
padding: 1.5rem;
.navbar-item .navbar-child {
padding-left: 1.5rem;
}
.navbar-dropdown.is-boxed.has-been-clicked {
// trick to unhover the menu dropdown
display: none !important;
}
</style>
<template>
<div class="container">
<div class="columns">
<nav class="panel column is-one-quarter">
<p class="panel-heading">
{{explore.settings.label}}
</p>
<div class="panel-block">
<p class="control">
<input class="input is-small" type="text" placeholder="search">
</p>
</div>
<div class="inner-scroll">
<template v-for="view in explore.views">
<a class="panel-block panel-block-heading has-background-white-ter has-text-grey-light" :key="view.unique_name">
{{view.settings.label}}
</a>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<a class="panel-block panel-block-heading has-background-white-ter has-text-grey-light">
Dimensions
</a>
<a class="panel-block" v-for="dimension in view.dimensions" :key="dimension.unique_name">
{{dimension | printable | underscoreToSpace | capitalize}}
</a>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<a class="panel-block panel-block-heading has-background-white-ter has-text-grey-light">
Measures
</a>
<a class="panel-block" v-for="measure in view.measures" :key="measure.unique_name">
{{measure | printable | underscoreToSpace | capitalize}}
</a>
</template>
</div>
<div class="panel-block">
<button class="button is-link is-outlined is-fullwidth">
reset all filters
</button>
</div>
</nav>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'Explore',
created() {
this.$store.dispatch('explores/getExplore', {
model: this.$route.params.model,
explore: this.$route.params.explore,
});
},
filters: {
printable(value) {
return value.settings.label ? value.settings.label : value.name;
},
},
computed: {
...mapState('explores', [
'explore',
]),
},
};
</script>
<style lang="scss" scoped>
.panel {
margin-top: 10px;
}
.panel-block {
&.panel-block-heading {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: bold;
&:hover {
background: white;
}
}
}
.inner-scroll {
position: relative;
height: calc(100vh - 252px);
overflow: scroll;
}
</style>
......@@ -41,7 +41,7 @@ import { mapState, mapGetters } from 'vuex';
export default {
name: 'Project',
created() {
this.getProjects();
this.getProjects(true);
},
computed: {
...mapState('projects', {
......
<template>
<div class="container">
<div class="columns">
<aside class="sidebar column is-one-quarter menu has-background-white-bis">
<aside class="fixed-sidebar column is-one-quarter menu has-background-white-bis">
<div class="level">
<a
href="#"
......@@ -119,18 +119,10 @@ export default {
};
</script>
<style lang="scss" scoped>
.sidebar {
position: fixed;
overflow: scroll;
top: 72px;
bottom: 0;
left: 0;
}
.content {
padding: 10px;
position: fixed;
top: 72px;
top: 52px;
left: 430px;
right: 0;
bottom: 0;
......
......@@ -12,3 +12,13 @@ new Vue({
router,
render: h => h(App),
});
Vue.filter('capitalize', (value) => {
if (!value) return '';
let newVal = value;
newVal = newVal.toString();
return newVal.charAt(0).toUpperCase() + newVal.slice(1);
});
Vue.filter('camelToRegular', value => value.replace(/([A-Z])/g, ' $1'));
Vue.filter('underscoreToSpace', value => value.replace(/_/g, ' '));
......@@ -3,6 +3,7 @@ import Router from 'vue-router';
import Project from '@/components/projects/Project';
import NewProjectForm from '@/components/projects/NewProjectForm';
import Repo from '@/components/repos/Repo';
import Explore from '@/components/explores/Explore';
Vue.use(Router);
......@@ -28,5 +29,10 @@ export default new Router({
name: 'Repo',
component: Repo,
},
{
path: '/explore/:model/:explore',
name: '',
component: Explore,
},
],
});
......@@ -24,3 +24,11 @@ $primary-invert: $pink-invert;
*/
@import "../../node_modules/bulma/bulma";
.fixed-sidebar {
position: fixed;
overflow: scroll;
top: 52px;
bottom: 0;
left: 0;
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import projects from './modules/projects';
import repos from './modules/repos';
import explores from './modules/explores';
Vue.use(Vuex);
......@@ -11,6 +12,7 @@ export default new Vuex.Store({
modules: {
projects,
repos,
explores,
},
string: debug,
});
import exploreApi from '../../api/explore';
const state = {
explore: {
settings: {
label: 'loading...',
},
},
};
const getters = {};
const actions = {
getExplore({ commit }, { model, explore }) {
exploreApi.index(model, explore)
.then((data) => {
commit('setExplore', data.data);
});
},
};
const mutations = {
setExplore(_, exploreData) {
state.explore = exploreData;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
......@@ -12,13 +12,13 @@ const getters = {
};
const actions = {
getProjects(context) {
getProjects(context, withRepo = false) {
projectApi.index()
.then((data) => {
context.commit('setProjects', {
project: data.data,
});
if (context.state.project.git_url) {
if (withRepo && context.state.project.git_url) {
this.dispatch('repos/getRepo');
}
});
......
......@@ -6,6 +6,8 @@ const state = {
loadingValidation: false,
loadingUpdate: false,
importResults: {},
models: [],
navbarClicked: false,
errors: [],
blobs: {
dashboards:
......@@ -45,6 +47,7 @@ const actions = {
});
},
getBlob({ commit }, blob) {
repoApi.blob(blob.hexsha)
.then((data) => {
......@@ -69,9 +72,31 @@ const actions = {
state.loadingUpdate = false;
});
},
getModels({ commit }) {
repoApi.models()
.then((data) => {
commit('setModels', data.data);
});
},
navbarHideDropdown({ commit }) {
commit('setHiddenDropdown');
},
};
const mutations = {
setHiddenDropdown() {
state.navbarClicked = true;
setTimeout(() => {
state.navbarClicked = false;
}, 500);
},
setModels(_, models) {
state.models = models;
},
setRepoBlobs(_, { blobs }) {
state.blobs = blobs;
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment