From 8792e9fe1c7ab76c35f9a18d866880ba3da2c13e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 15 Mar 2021 19:09:37 +0100 Subject: move frontend site folder. add video support --- cli/app/settings/app_cfg.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'cli/app/settings/app_cfg.py') diff --git a/cli/app/settings/app_cfg.py b/cli/app/settings/app_cfg.py index 5fc4982..3865abd 100644 --- a/cli/app/settings/app_cfg.py +++ b/cli/app/settings/app_cfg.py @@ -63,6 +63,8 @@ DIR_STATIC = join(DIR_APP, 'static') HASH_TREE_DEPTH = 3 # for sha256 subdirs HASH_BRANCH_SIZE = 3 # for sha256 subdirs +DIR_PUBLIC_EXPORTS = os.getenv('DIR_PUBLIC_EXPORTS') or DIR_EXPORTS + # ----------------------------------------------------------------------------- # S3 storage -- cgit v1.2.3-70-g09d2 From 1cfe96ca6ef5c54eadd986c951dade0f56d72440 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 16 Mar 2021 16:54:28 +0100 Subject: migrating to mysql --- cli/app/settings/app_cfg.py | 6 ++ cli/app/sql/common.py | 23 ++-- cli/app/sql/env.py | 2 +- cli/app/sql/models/graph.py | 3 + cli/app/sql/models/upload.py | 4 +- .../202103161645_add_foreign_key_constraint.py | 29 +++++ cli/commands/admin/migrate_to_mysql.py | 120 +++++++++++++++++++++ docs/migrate_to_mysql.md | 1 + env-sample | 7 ++ frontend/app/views/index/containers/graph.index.js | 1 + 10 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 cli/app/sql/versions/202103161645_add_foreign_key_constraint.py create mode 100644 cli/commands/admin/migrate_to_mysql.py create mode 100644 docs/migrate_to_mysql.md (limited to 'cli/app/settings/app_cfg.py') diff --git a/cli/app/settings/app_cfg.py b/cli/app/settings/app_cfg.py index 3865abd..af6cf89 100644 --- a/cli/app/settings/app_cfg.py +++ b/cli/app/settings/app_cfg.py @@ -33,6 +33,7 @@ load_dotenv(dotenv_path=fp_env) CLICK_GROUPS = { # 'process': 'commands/process', 'site': 'commands/site', + 'admin': 'commands/admin', 'db': '', 'flask': '', } @@ -65,6 +66,11 @@ HASH_BRANCH_SIZE = 3 # for sha256 subdirs DIR_PUBLIC_EXPORTS = os.getenv('DIR_PUBLIC_EXPORTS') or DIR_EXPORTS +# ----------------------------------------------------------------------------- +# Database +# ----------------------------------------------------------------------------- + +USE_SQLITE = os.getenv("USE_SQLITE") == "True" # ----------------------------------------------------------------------------- # S3 storage diff --git a/cli/app/sql/common.py b/cli/app/sql/common.py index c8bd557..8e1d2b3 100644 --- a/cli/app/sql/common.py +++ b/cli/app/sql/common.py @@ -2,7 +2,6 @@ import os import glob import time -# import mysql.connector from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base @@ -11,16 +10,16 @@ from flask_sqlalchemy import SQLAlchemy from app.settings import app_cfg -# connection_url = "mysql+mysqlconnector://{}:{}@{}/{}?charset=utf8mb4".format( -# os.getenv("DB_USER"), -# os.getenv("DB_PASS"), -# os.getenv("DB_HOST"), -# os.getenv("DB_NAME") -# ) - -os.makedirs(app_cfg.DIR_DATABASE, exist_ok=True) - -connection_url = "sqlite:///{}".format(os.path.join(app_cfg.DIR_DATABASE, 'swimmer.sqlite3')) +if app_cfg.USE_SQLITE: + os.makedirs(app_cfg.DIR_DATABASE, exist_ok=True) + connection_url = "sqlite:///{}".format(os.path.join(app_cfg.DIR_DATABASE, 'swimmer.sqlite3')) +else: + connection_url = "mysql+pymysql://{}:{}@{}/{}?charset=utf8mb4".format( + os.getenv("DB_USER"), + os.getenv("DB_PASS"), + os.getenv("DB_HOST"), + os.getenv("DB_NAME") + ) engine = create_engine(connection_url, encoding="utf-8", pool_recycle=3600) @@ -31,7 +30,7 @@ Base.metadata.bind = engine db = SQLAlchemy() # include the models in reverse dependency order, so relationships work +from app.sql.models.upload import Upload from app.sql.models.tile import Tile from app.sql.models.page import Page from app.sql.models.graph import Graph -from app.sql.models.upload import Upload diff --git a/cli/app/sql/env.py b/cli/app/sql/env.py index 7753565..3e015b5 100644 --- a/cli/app/sql/env.py +++ b/cli/app/sql/env.py @@ -14,10 +14,10 @@ config.set_main_option("sqlalchemy.url", connection_url) target_metadata = Base.metadata # include the models in reverse dependency order, so relationships work +from app.sql.models.upload import Upload from app.sql.models.tile import Tile from app.sql.models.page import Page from app.sql.models.graph import Graph -from app.sql.models.upload import Upload def run_migrations_offline(): """Run migrations in 'offline' mode. diff --git a/cli/app/sql/models/graph.py b/cli/app/sql/models/graph.py index 8e068a0..08f4d3c 100644 --- a/cli/app/sql/models/graph.py +++ b/cli/app/sql/models/graph.py @@ -23,6 +23,7 @@ class Graph(Base): updated_at = Column(UtcDateTime(), onupdate=utcnow()) pages = relationship('Page', lazy='dynamic') + uploads = relationship('Upload', lazy='dynamic') def toJSON(self): return { @@ -40,11 +41,13 @@ class Graph(Base): def toFullJSON(self): data = self.toJSON() data['pages'] = [ page.toLinkJSON() for page in self.pages ] + data['uploads'] = [ upload.toJSON() for upload in self.uploads ] return data def toSiteJSON(self): data = self.toJSON() data['pages'] = [ page.toFullJSON() for page in self.pages ] + data['uploads'] = [ upload.toJSON() for upload in self.uploads ] return data class GraphForm(ModelForm): diff --git a/cli/app/sql/models/upload.py b/cli/app/sql/models/upload.py index 87f758a..30e53dc 100644 --- a/cli/app/sql/models/upload.py +++ b/cli/app/sql/models/upload.py @@ -1,4 +1,4 @@ -from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime +from sqlalchemy import create_engine, Table, Column, ForeignKey, String, Integer, DateTime import sqlalchemy.sql.functions as func from sqlalchemy_utc import UtcDateTime, utcnow from wtforms_alchemy import ModelForm @@ -14,7 +14,7 @@ class Upload(Base): """Table for storing references to various media""" __tablename__ = 'upload' id = Column(Integer, primary_key=True) - graph_id = Column(Integer) + graph_id = Column(Integer, ForeignKey('graph.id'), nullable=True) sha256 = Column(String(256), nullable=False) fn = Column(String(256), nullable=False) ext = Column(String(4, convert_unicode=True), nullable=False) diff --git a/cli/app/sql/versions/202103161645_add_foreign_key_constraint.py b/cli/app/sql/versions/202103161645_add_foreign_key_constraint.py new file mode 100644 index 0000000..673f9e4 --- /dev/null +++ b/cli/app/sql/versions/202103161645_add_foreign_key_constraint.py @@ -0,0 +1,29 @@ +"""add foreign key constraint + +Revision ID: 3f7df6bf63b8 +Revises: 645f315e651d +Create Date: 2021-03-16 16:45:39.455892 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = '3f7df6bf63b8' +down_revision = '645f315e651d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key(None, 'upload', 'graph', ['graph_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'upload', type_='foreignkey') + # ### end Alembic commands ### diff --git a/cli/commands/admin/migrate_to_mysql.py b/cli/commands/admin/migrate_to_mysql.py new file mode 100644 index 0000000..32ff7cf --- /dev/null +++ b/cli/commands/admin/migrate_to_mysql.py @@ -0,0 +1,120 @@ +import click +import os +import glob +import time + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from flask_sqlalchemy import SQLAlchemy + +from app.settings import app_cfg + +@click.command('migrate_to_mysql') +@click.pass_context +def cli(ctx): + """ + - Create connections to both databases + - For each table, for each row, insert from one to the other + """ + mysql_session, mysql_base = make_mysql_base() + sqlite_session, sqlite_base = make_sqlite3_base() + mysql_classes = make_classes(mysql_base) + sqlite_classes = make_classes(sqlite_base) + + for mysql_class, sqlite_class in zip(mysql_classes, sqlite_classes): + sqlite_objs = sqlite_session.query(sqlite_class).order_by(sqlite_class.id).all() + for sqlite_obj in sqlite_objs: + mysql_obj = mysql_class() + for column in sqlite_class.__table__.columns: + table_name, column_name = str(column).split(".") + # print(f"{table_name} => {column_name}") + # if column_name != 'id': + setattr(mysql_obj, column_name, getattr(sqlite_obj, column_name)) + mysql_session.add(mysql_obj) + mysql_session.commit() + +def make_mysql_base(): + """Make a Mysql connection""" + connection_url = "mysql+pymysql://{}:{}@{}/{}?charset=utf8mb4".format( + os.getenv("DB_USER"), + os.getenv("DB_PASS"), + os.getenv("DB_HOST"), + os.getenv("DB_NAME") + ) + return make_base(connection_url) + +def make_sqlite3_base(): + """Make a SQLite3 connection""" + connection_url = "sqlite:///{}".format(os.path.join(app_cfg.DIR_DATABASE, 'animism.sqlite3')) + return make_base(connection_url) + +def make_base(connection_url): + """Make a connection base from a connection URL""" + engine = create_engine(connection_url, encoding="utf-8", pool_recycle=3600) + Session = sessionmaker(bind=engine) + Base = declarative_base() + Base.metadata.bind = engine + db = SQLAlchemy() + return Session(), Base + +def make_classes(Base): + """Make classes from a base""" + + from sqlalchemy import create_engine, Table, Column, Text, String, Integer, \ + Boolean, Float, DateTime, JSON, ForeignKey + from sqlalchemy_utc import UtcDateTime, utcnow + + class Upload(Base): + """Table for storing references to various media""" + __tablename__ = 'upload' + id = Column(Integer, primary_key=True) + graph_id = Column(Integer, ForeignKey('graph.id'), nullable=True) + sha256 = Column(String(256), nullable=False) + fn = Column(String(256), nullable=False) + ext = Column(String(4, convert_unicode=True), nullable=False) + tag = Column(String(64, convert_unicode=True), nullable=True) + username = Column(String(16, convert_unicode=True), nullable=False) + created_at = Column(UtcDateTime(), default=utcnow()) + + class Tile(Base): + """Table for storing references to tiles""" + __tablename__ = 'tile' + id = Column(Integer, primary_key=True) + graph_id = Column(Integer, ForeignKey('graph.id'), nullable=True) + page_id = Column(Integer, ForeignKey('page.id'), nullable=True) + target_page_id = Column(Integer, ForeignKey('page.id'), nullable=True) + type = Column(String(16, convert_unicode=True), nullable=False) + sort_order = Column(Integer, default=0) + settings = Column(JSON, default={}, nullable=True) + created_at = Column(UtcDateTime(), default=utcnow()) + updated_at = Column(UtcDateTime(), onupdate=utcnow()) + + class Page(Base): + """Table for storing references to pages""" + __tablename__ = 'page' + id = Column(Integer, primary_key=True) + graph_id = Column(Integer, ForeignKey('graph.id'), nullable=True) + path = Column(String(64, convert_unicode=True), nullable=False) + title = Column(String(64, convert_unicode=True), nullable=False) + username = Column(String(32, convert_unicode=True), nullable=False) + description = Column(Text(convert_unicode=True), nullable=False) + settings = Column(JSON, default={}, nullable=True) + created_at = Column(UtcDateTime(), default=utcnow()) + updated_at = Column(UtcDateTime(), onupdate=utcnow()) + + class Graph(Base): + """Table for storing references to graphs""" + __tablename__ = 'graph' + id = Column(Integer, primary_key=True) + home_page_id = Column(Integer, nullable=True) + path = Column(String(64, convert_unicode=True), nullable=False) + title = Column(String(64, convert_unicode=True), nullable=False) + username = Column(String(32, convert_unicode=True), nullable=False) + description = Column(Text(convert_unicode=True), nullable=False) + settings = Column(JSON, default={}, nullable=True) + created_at = Column(UtcDateTime(), default=utcnow()) + updated_at = Column(UtcDateTime(), onupdate=utcnow()) + + return [ Upload, Graph, Page, Tile ] diff --git a/docs/migrate_to_mysql.md b/docs/migrate_to_mysql.md new file mode 100644 index 0000000..45fe7de --- /dev/null +++ b/docs/migrate_to_mysql.md @@ -0,0 +1 @@ +migrate_to_mysql.md diff --git a/env-sample b/env-sample index 1a23be3..83c36e4 100644 --- a/env-sample +++ b/env-sample @@ -2,3 +2,10 @@ FLASK_RUN_PORT=7500 SERVER_NAME=swim.neural.garden HTTP_EXTERNAL_HOST=https://swim.neural.garden +USE_SQLITE=False + +DB_USER=swimmer +DB_PASS= +DB_HOST=localhost +DB_NAME=swimmer + diff --git a/frontend/app/views/index/containers/graph.index.js b/frontend/app/views/index/containers/graph.index.js index 91098a7..bf3d75e 100644 --- a/frontend/app/views/index/containers/graph.index.js +++ b/frontend/app/views/index/containers/graph.index.js @@ -11,6 +11,7 @@ class GraphIndex extends Component { componentDidMount() { actions.graph.index() } + render() { const { index } = this.props // console.log(this.props) -- cgit v1.2.3-70-g09d2 From d165a0727e42349d935ab3ee287242f1e5029742 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Wed, 17 Mar 2021 18:11:26 +0100 Subject: frontend. export/view button. interactivity sanity check --- cli/app/controllers/graph_controller.py | 11 +- cli/app/server/demo.py | 48 + cli/app/server/web.py | 2 +- cli/app/settings/app_cfg.py | 7 +- cli/app/site/export.py | 23 +- cli/cli.py | 7 + cli/commands/site/export.py | 6 +- env-sample | 1 + frontend/app/common/app.css | 14 + frontend/app/utils/index.js | 3 +- .../app/views/audio/components/audio.select.js | 11 +- frontend/app/views/graph/components/page.form.js | 10 +- frontend/app/views/graph/graph.actions.js | 11 +- frontend/app/views/graph/graph.reducer.js | 14 + frontend/app/views/page/components/page.header.js | 18 +- frontend/app/views/page/components/tile.form.js | 41 +- frontend/app/views/page/components/tile.handle.js | 1 - frontend/site/audio/audio.player.js | 43 + frontend/site/index.js | 2 + frontend/site/site.css | 20 + frontend/site/site/site.actions.js | 4 + frontend/site/site/site.reducer.js | 7 + frontend/site/store.js | 3 +- frontend/site/types.js | 4 +- frontend/site/viewer/viewer.container.js | 60 +- package-lock.json | 11491 ------------------- package.json | 4 +- webpack.config.dev.js | 45 +- webpack.config.prod.js | 55 +- yarn.lock | 5 + 30 files changed, 391 insertions(+), 11580 deletions(-) create mode 100644 cli/app/server/demo.py create mode 100644 frontend/site/audio/audio.player.js create mode 100644 frontend/site/site.css delete mode 100644 package-lock.json (limited to 'cli/app/settings/app_cfg.py') diff --git a/cli/app/controllers/graph_controller.py b/cli/app/controllers/graph_controller.py index 7efda73..fcca50a 100644 --- a/cli/app/controllers/graph_controller.py +++ b/cli/app/controllers/graph_controller.py @@ -7,6 +7,7 @@ from app.sql.models.graph import Graph, GraphForm from app.sql.models.page import Page from app.sql.models.tile import Tile from app.controllers.crud_controller import CrudView +from app.site.export import export_site class GraphView(CrudView): model = Graph @@ -20,7 +21,7 @@ class GraphView(CrudView): @route('/name/', methods=['GET']) def get_name(self, graph_path: str): """ - Fetch a single {model}. + Fetch a single graph. """ session = Session() item = session.query(self.model).filter(self.model.path == graph_path).first() @@ -36,3 +37,11 @@ class GraphView(CrudView): } session.close() return jsonify(result) + + @route('/export/', methods=['GET']) + def export(self, graph_path: str): + export_site(opt_graph_path=graph_path) + result = { + 'status': 'ok', + } + return jsonify(result) diff --git a/cli/app/server/demo.py b/cli/app/server/demo.py new file mode 100644 index 0000000..847f95b --- /dev/null +++ b/cli/app/server/demo.py @@ -0,0 +1,48 @@ +import os +import logging +import logging.handlers + +logger = logging.getLogger("") +logger.setLevel(logging.DEBUG) +handler = logging.handlers.RotatingFileHandler("flask.log", + maxBytes=3000000, backupCount=2) +formatter = logging.Formatter( + '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) +logging.getLogger().addHandler(logging.StreamHandler()) + +from flask import Flask, send_from_directory, request + +from app.settings import app_cfg + +def create_demo_app(script_info=None): + """ + functional pattern for creating the flask app + """ + logging.debug("Starting Swimmer demo server...") + app = Flask(__name__, static_folder=app_cfg.DIR_EXPORTS, static_url_path='/') + app.config['SERVER_NAME'] = app_cfg.DEMO_SERVER_NAME + app.url_map.strict_slashes = False + + @app.errorhandler(404) + def not_found(error): + path, fn = os.path.split(request.path) + path = path[1:] + dir_path = os.path.join(app_cfg.DIR_EXPORTS, path) + if os.path.isfile(os.path.join(dir_path, fn)): + return send_from_directory(dir_path, fn) + if os.path.isfile(os.path.join(dir_path, fn, 'index.html')): + return send_from_directory(os.path.join(dir_path, fn), 'index.html') + return "404", 404 + + @app.route('/') + def serve_index(): + return "Swimmer demo", 200 + + @app.route('/favicon.ico') + def favicon(): + return send_from_directory(os.path.join(app_cfg.DIR_STATIC, 'img'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + return app diff --git a/cli/app/server/web.py b/cli/app/server/web.py index 1a3b064..5eb172c 100644 --- a/cli/app/server/web.py +++ b/cli/app/server/web.py @@ -52,7 +52,7 @@ def create_app(script_info=None): @app.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(app.root_path, 'static/img/'), - 'favicon.ico',mimetype='image/vnd.microsoft.icon') + 'favicon.ico', mimetype='image/vnd.microsoft.icon') @app.shell_context_processor def shell_context(): diff --git a/cli/app/settings/app_cfg.py b/cli/app/settings/app_cfg.py index af6cf89..4aa4bee 100644 --- a/cli/app/settings/app_cfg.py +++ b/cli/app/settings/app_cfg.py @@ -15,9 +15,10 @@ codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else Non LOG = logging.getLogger('swimmer') # ----------------------------------------------------------------------------- -# .env config for keys +# .env config # ----------------------------------------------------------------------------- # Project directory + SELF_CWD = os.path.dirname(os.path.realpath(__file__)) # this file DIR_PROJECT_ROOT = str(Path(SELF_CWD).parent.parent.parent) @@ -31,14 +32,13 @@ load_dotenv(dotenv_path=fp_env) # ----------------------------------------------------------------------------- CLICK_GROUPS = { - # 'process': 'commands/process', 'site': 'commands/site', 'admin': 'commands/admin', 'db': '', 'flask': '', + 'demo': '', } - # ----------------------------------------------------------------------------- # File I/O # ----------------------------------------------------------------------------- @@ -86,6 +86,7 @@ except Exception as e: # ----------------------------------------------------------------------------- SERVER_NAME = os.getenv('SERVER_NAME') or '0.0.0.0:5000' +DEMO_SERVER_NAME = os.getenv('DEMO_SERVER_NAME') or '0.0.0.0:3000' HTTP_EXTERNAL_HOST = os.getenv('HTTP_EXTERNAL_HOST') or f"http://{SERVER_NAME}" # ----------------------------------------------------------------------------- diff --git a/cli/app/site/export.py b/cli/app/site/export.py index d513286..c301a60 100644 --- a/cli/app/site/export.py +++ b/cli/app/site/export.py @@ -8,7 +8,7 @@ import os from app.sql.common import db, Session, Graph, Page, Tile from distutils.dir_util import copy_tree -def export_site(ctx, opt_graph_path, opt_output_dir): +def export_site(opt_graph_path, opt_output_dir=app_cfg.DIR_EXPORTS, opt_build_js=False): """Export a graph""" # ------------------------------------------------ @@ -42,6 +42,7 @@ def export_site(ctx, opt_graph_path, opt_output_dir): home_page = site_data['graph']['home_page'] if home_page is None: print("Homepage not set! Shift-click a page on the graph to make it the homepage.") + session.close() return write_text(f'', join(graph_dir, 'index.html')) @@ -55,8 +56,10 @@ def export_site(ctx, opt_graph_path, opt_output_dir): print(f'/{page_path}') write_index(graph, page, index_html, join(graph_dir, page.path, 'index.html')) - build_javascript(graph_dir) + if opt_build_js or not os.path.exists(f"{graph_dir}/bundle.js"): + build_javascript(graph_dir) + session.close() print("Site export complete!") print(f"Graph exported to: {graph_dir}") @@ -86,22 +89,34 @@ def sanitize_graph(graph): page_path_lookup[page['id']] = page_path for page in graph['pages']: sanitize_page(page) - if page['id'] == 12: - print(page) for tile in page['tiles']: if tile['target_page_id']: if tile['target_page_id'] == -1: tile['href'] = tile['settings']['external_link_url'] elif tile['target_page_id'] > 0: tile['href'] = page_path_lookup[tile['target_page_id']] + if 'url' in tile['settings'] and tile['settings']['url'].startswith('/static'): + tile['settings']['url'] = '/' + graph['path'] + tile['settings']['url'] sanitize_tile(tile) page_path = page_path_lookup[page['id']] page_lookup[page_path] = page + for upload in graph['uploads']: + sanitize_upload(upload) + if upload['url'].startswith('/static'): + upload['url'] = '/' + graph['path'] + upload['url'] # print(page_lookup['/asdf/testttt']) graph['pages'] = page_lookup graph['home_page'] = page_path_lookup[graph['home_page_id']] return graph +def sanitize_upload(data): + if 'created_at' in data: + del data['created_at'] + if 'username' in data: + del data['username'] + if 'graph_id' in data: + del data['graph_id'] + def sanitize_page(data): if 'created_at' in data: del data['created_at'] diff --git a/cli/cli.py b/cli/cli.py index 2158398..3534c43 100755 --- a/cli/cli.py +++ b/cli/cli.py @@ -29,6 +29,13 @@ if __name__ == '__main__': cli = FlaskGroup(create_app=create_app) + elif args.group == 'demo': + + from flask.cli import FlaskGroup + from app.server.demo import create_demo_app + + cli = FlaskGroup(create_app=create_demo_app) + elif args.group == 'db': import re diff --git a/cli/commands/site/export.py b/cli/commands/site/export.py index 68773e9..78e7228 100644 --- a/cli/commands/site/export.py +++ b/cli/commands/site/export.py @@ -8,8 +8,10 @@ from app.site.export import export_site help='Graph name') @click.option('-o', '--output', 'opt_output_dir', required=True, default=app_cfg.DIR_EXPORTS, help='Output dir') +@click.option('-j', '--js/--no-js', 'opt_js', required=False, default=False, + help='Whether to rebuild the Javascript bundle') @click.pass_context -def cli(ctx, opt_graph_path, opt_output_dir): +def cli(ctx, opt_graph_path, opt_output_dir, opt_build_js): """Export a graph""" - export_site(opt_graph_path, opt_output_dir) \ No newline at end of file + export_site(opt_graph_path, opt_output_dir, opt_build_js) \ No newline at end of file diff --git a/env-sample b/env-sample index 83c36e4..6df55d5 100644 --- a/env-sample +++ b/env-sample @@ -1,6 +1,7 @@ FLASK_RUN_PORT=7500 SERVER_NAME=swim.neural.garden HTTP_EXTERNAL_HOST=https://swim.neural.garden +EXPORT_HOST=http://3.k:3000 USE_SQLITE=False diff --git a/frontend/app/common/app.css b/frontend/app/common/app.css index 2e9dc4e..486e5fa 100644 --- a/frontend/app/common/app.css +++ b/frontend/app/common/app.css @@ -116,6 +116,20 @@ header > div > button:hover { border-color: #fff; color: #fff; } + +header .building { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + margin-left: 1rem; + color: #888; +} +header .building .loader { + transform: scale(0.75); + margin-right: 0.5rem; +} + header .vcat-btn { font-size: 0.875rem; padding-left: 0.5rem; diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js index 1114d65..6e5a909 100644 --- a/frontend/app/utils/index.js +++ b/frontend/app/utils/index.js @@ -50,7 +50,8 @@ export const pad = (n, m) => { } export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's') - +export const capitalize = s => s.split(' ').map(capitalizeWord).join(' ') +export const capitalizeWord = s => s.substr(0, 1).toUpperCase() + s.substr(1) export const padSeconds = n => n < 10 ? '0' + n : n export const timestamp = (n = 0, fps = 25) => { diff --git a/frontend/app/views/audio/components/audio.select.js b/frontend/app/views/audio/components/audio.select.js index 73142f0..cf1dfb2 100644 --- a/frontend/app/views/audio/components/audio.select.js +++ b/frontend/app/views/audio/components/audio.select.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { Select } from 'app/common' +import { unslugify } from 'app/utils' const NO_AUDIO = 0 const AUDIO_TOP_OPTIONS = [ @@ -16,14 +17,14 @@ class AudioSelect extends Component { constructor(props) { super(props) - this.handleSelect = this.handleSelect.bind(this) + this.handleChange = this.handleChange.bind(this) } componentDidMount(){ const { uploads } = this.props.graph.show.res const audioUploads = uploads .filter(upload => upload.tag === 'audio') - .map(page => ({ name: upload.id, label: page.path })) + .map(upload => ({ name: upload.id, label: unslugify(upload.fn) })) let audioList = [ ...AUDIO_TOP_OPTIONS, ...audioUploads, @@ -33,6 +34,10 @@ class AudioSelect extends Component { }) } + handleChange(name, value) { + this.props.onChange(name, parseInt(value)) + } + render() { return (