├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── crux-console-conf.sample.edn ├── dev ├── build-ebs.sh ├── build-report--app-8f3b217f.html ├── dep-tree.txt ├── dev.clj ├── log_dev_app.properties ├── reqs.clj ├── requests.sh ├── shadow-dev.sh ├── shadow-prod.sh └── update-jar-app-js.sh ├── doc ├── README.adoc ├── get_started.adoc └── resources │ ├── doc │ ├── asciidoctor │ │ └── attributes.edn │ └── docinfo │ │ ├── docinfo-footer.html │ │ └── docinfo.html │ └── img │ └── console1-runqempty.gif ├── ebs ├── .ebextensions │ ├── https-instance-securitygroup.config │ └── nginx │ │ ├── conf.d │ │ └── console.conf │ │ └── nginx.conf ├── generate-dh.sh ├── readme.md ├── sample-console-install.config └── stack.yaml ├── package.json ├── project.clj ├── readme.md ├── resources ├── screenshot-1.png └── static │ ├── img │ ├── console-logo.svg │ ├── crux-labs.png │ ├── crux-labs.svg │ ├── crux-logo.svg │ ├── cube-for-icon.svg │ ├── cube-on-white-120.png │ ├── cube-on-white-192.png │ ├── cube-on-white-512.png │ ├── cube.svg │ └── juxt-logo.svg │ ├── plots-data.edn │ └── styles │ ├── codemirror.css │ ├── eclipse.css │ ├── main.css │ ├── monokai.css │ ├── normalize.css │ ├── react-input-range.css │ ├── react-ui-tree.css │ └── reset.css ├── shadow-cljs.edn ├── src ├── crux_ui │ ├── better_printer.cljs │ ├── config.cljs │ ├── cookies.cljs │ ├── events │ │ ├── default_db.cljs │ │ ├── effects.cljs │ │ └── facade.cljs │ ├── functions.cljs │ ├── io │ │ └── query.cljs │ ├── logging.cljs │ ├── logic │ │ ├── example_queries.cljs │ │ ├── example_txes_amzn.cljs │ │ ├── history_perversions.cljs │ │ ├── plotting_data_calc.cljs │ │ ├── query_analysis.cljs │ │ ├── readme.md │ │ └── time.cljs │ ├── main.cljs │ ├── main_perf.cljs │ ├── routes.cljs │ ├── subs.cljs │ ├── svg_icons.cljs │ ├── ua-regexp.js │ └── views │ │ ├── attr_stats.cljs │ │ ├── charts │ │ ├── custom-plotly--console.js │ │ ├── custom-plotly--perf.js │ │ ├── query_perf_plots.cljs │ │ ├── wrapper_3d.cljs │ │ └── wrapper_basic.cljs │ │ ├── cluster_health.cljs │ │ ├── codemirror.cljs │ │ ├── commons │ │ ├── contenteditable.cljs │ │ ├── css_cube.cljs │ │ ├── css_logo.cljs │ │ ├── cube_svg.cljs │ │ ├── datepicker_native.cljs │ │ ├── dom.cljs │ │ ├── form_line.cljs │ │ ├── input.cljs │ │ ├── keycodes.cljs │ │ ├── tabs.cljs │ │ ├── tiny_components.cljs │ │ └── user_intents.cljs │ │ ├── facade.cljs │ │ ├── facade_perf.cljs │ │ ├── functions.cljs │ │ ├── header.cljs │ │ ├── node_status.cljs │ │ ├── output │ │ ├── attr_history.cljs │ │ ├── edn.cljs │ │ ├── error.cljs │ │ ├── react_tree.cljs │ │ ├── table.cljs │ │ ├── tree.cljs │ │ └── tx_history.cljs │ │ ├── overview.cljs │ │ ├── query │ │ ├── datepicker_slider.cljs │ │ ├── editor.cljs │ │ ├── examples.cljs │ │ ├── form.cljs │ │ ├── output.cljs │ │ └── time_controls.cljs │ │ ├── query_perf.cljs │ │ ├── query_ui.cljs │ │ ├── second_layer.cljs │ │ ├── settings.cljs │ │ ├── sidebar.cljs │ │ └── style.cljs ├── crux_ui_lib │ ├── async_http_client.cljc │ ├── functions.cljc │ └── http_functions.cljs ├── crux_ui_server │ ├── config.clj │ ├── crux_auto_start.clj │ ├── generate_web_manifest.clj │ ├── main.clj │ ├── pages.clj │ └── preloader.clj ├── dev.clj └── public │ ├── logo.css │ ├── main.css │ └── normalize.css ├── test └── crux_console │ ├── query_analysis_test.cljs │ └── test_runner.cljs └── yarn.lock /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | jobs: 6 | #lint: 7 | # runs-on: ubuntu-latest 8 | # steps: 9 | # - uses: actions/checkout@v1 10 | # - name: Run linter 11 | # uses: DeLaGuardo/clojure-lint-action@v1 12 | # with: 13 | # clj-kondo-args: --lint src 14 | # github_token: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | jars: 17 | # needs: lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Git checkout 21 | uses: actions/checkout@v1 22 | 23 | - name: Setup java 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: '13' 27 | 28 | - name: Setup clojure 29 | uses: DeLaGuardo/setup-clojure@2.0 30 | with: 31 | tools-deps: latest 32 | 33 | - name: Cache clojure deps 34 | uses: actions/cache@v1 35 | with: 36 | path: ~/.m2/repository 37 | key: clojure-${{ hashFiles('**/project.clj') }} 38 | restore-keys: | 39 | clojure- 40 | 41 | - name: Run clean 42 | run: make clean 43 | 44 | - name: install node modules 45 | run: make install 46 | 47 | - name: Get yarn cache 48 | id: yarn-cache 49 | run: echo "::set-output name=dir::$(yarn cache dir)" 50 | 51 | - uses: actions/cache@v1 52 | with: 53 | path: ${{ steps.yarn-cache.outputs.dir }} 54 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 55 | restore-keys: | 56 | ${{ runner.os }}-yarn- 57 | 58 | - name: build web 59 | run: make build 60 | 61 | - name: build uberjar 62 | run: make jar 63 | 64 | - name: upload skimmed jar 65 | uses: actions/upload-artifact@v1 66 | with: 67 | name: crux-console-skimmed.jar 68 | path: target/crux-console-skimmed.jar 69 | 70 | - name: upload the jar with crux 71 | uses: actions/upload-artifact@v1 72 | with: 73 | name: crux-console.jar 74 | path: target/crux-console.jar 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.cpcache 2 | **/.rebel_readline_history 3 | **/.nrepl-port 4 | **/target/*/** 5 | target 6 | **/log/* 7 | !**/target/*/.gitkeep 8 | data 9 | crux-console-conf.edn 10 | resources/static/crux-ui/compiled 11 | resources/static/crux-ui/test 12 | node_modules 13 | .shadow-cljs 14 | ebs/ebs-uber.jar 15 | ebs/ebs-upload.zip 16 | 17 | # pack'd output 18 | /*.jar 19 | **/*.jar 20 | 21 | # Elastic Beanstalk Files 22 | .elasticbeanstalk/* 23 | !.elasticbeanstalk/*.cfg.yml 24 | !.elasticbeanstalk/*.global.yml 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016-2019 JUXT LTD. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean install build jar 2 | 3 | clean: 4 | rm -rf target 5 | 6 | install: 7 | yarn install 8 | 9 | build: 10 | node_modules/.bin/shadow-cljs release app 11 | 12 | build-perf: 13 | node_modules/.bin/shadow-cljs release app-perf 14 | 15 | jar: 16 | lein with-profile base:crux-jars uberjar 17 | -------------------------------------------------------------------------------- /crux-console-conf.sample.edn: -------------------------------------------------------------------------------- 1 | {:console/frontend-port 5000 2 | 3 | :console/embed-crux false 4 | ; set to true, if you want the console to start a Crux node for you 5 | ; meaning you should use the crux-console.jar 6 | 7 | :console/routes-prefix "/console" 8 | :console/hide-features #{:features/attribute-history} 9 | :console/crux-node-url-base "localhost:8080/crux" 10 | :console/crux-http-port 8080} -------------------------------------------------------------------------------- /dev/build-ebs.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | cp target/crux-console.jar ebs/ebs-uber.jar 4 | 5 | # java -jar target/crux-console.jar 6 | 7 | ( 8 | cd ebs 9 | zip ebs-upload.zip ebs-uber.jar 10 | zip -r ebs-upload.zip .ebextensions 11 | ) 12 | 13 | # ./bin/capsule -m edge.main -A:prod ebs/ebs-uber.jar && cd ebs && zip ebs-upload.zip ebs-uber.jar && zip -r ebs-upload.zip .ebextensions 14 | -------------------------------------------------------------------------------- /dev/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require 3 | [dev-extras :refer :all])) 4 | 5 | ;; Add your helpers here 6 | -------------------------------------------------------------------------------- /dev/log_dev_app.properties: -------------------------------------------------------------------------------- 1 | app_root_logger=juxt.crux-ui 2 | -------------------------------------------------------------------------------- /dev/reqs.clj: -------------------------------------------------------------------------------- 1 | (require '[crux.api :as crux]) 2 | 3 | (def n 4 | (crux/start-node 5 | {:crux.node/topology :crux.standalone/topology 6 | :crux.node/kv-store "crux.kv.rocksdb/kv" 7 | :crux.standalone/event-log-dir "data/eventlog-2" 8 | :crux.kv/db-dir "data/db-dir-2"})) 9 | 10 | (crux/submit-tx n 11 | [[:crux.tx/put 12 | {:crux.db/id :ids/leno}] 13 | [:crux.tx/put 14 | {:crux.db/id :ids/deni}] 15 | [:crux.tx/put 16 | {:crux.db/id :ids/reni}]]) 17 | 18 | (crux/q (crux/db n) 19 | '[:find e 20 | :where 21 | [e :crux.db/id] 22 | [(contains? ids e)] 23 | :args {ids #{:ids/leno :ids/reni}} 24 | :limit 10]) 25 | 26 | (crux/q (crux/db n) 27 | '{:find [e] 28 | :args [{id [in #{:ids/leno}]}] 29 | :in [id] 30 | :where [[e :crux.db/id id]] 31 | :limit 1}) 32 | -------------------------------------------------------------------------------- /dev/requests.sh: -------------------------------------------------------------------------------- 1 | 2 | curl -v localhost:8080/query \ 3 | -H 'Content-Type: application/edn' \ 4 | -H "Origin: http://example.com" \ 5 | --data-binary \ 6 | "{:query 7 | [:find e 8 | :where 9 | [e :crux.db/id] 10 | [(contains? ids e)] 11 | :args {} 12 | :limit 10]}" 13 | 14 | # breaks, to test exception reporting 15 | curl -v localhost:8080/query \ 16 | -H 'Content-Type: application/edn' \ 17 | -H "Origin: http://example.com" \ 18 | --data-binary \ 19 | "{:query 20 | [:find e 21 | :where 22 | [e :crux.db/id] 23 | [(contains? ids e)] 24 | :args {} 25 | :limit 10]}" 26 | 27 | 28 | curl localhost:8080/query \ 29 | -H 'Content-Type: application/edn' \ 30 | --data-binary \ 31 | '{:query 32 | {:find [e] 33 | :where 34 | [[e :crux.db/id] 35 | [(contains? ids e)]] 36 | :args [{ids #{:ids/tech-ticker-2}}] 37 | :limit 2}}' 38 | 39 | -------------------------------------------------------------------------------- /dev/shadow-dev.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | node_modules/.bin/shadow-cljs watch dev 3 | 4 | # or 5 | # shadow-cljs watch dev-perf 6 | -------------------------------------------------------------------------------- /dev/shadow-prod.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | rm -r resources/static/crux-ui/compiled 4 | 5 | node_modules/.bin/shadow-cljs release app 6 | 7 | node_modules/.bin/shadow-cljs release app-perf 8 | 9 | gzip -9k resources/static/crux-ui/compiled/* 10 | 11 | ls -lah resources/static/crux-ui/compiled 12 | -------------------------------------------------------------------------------- /dev/update-jar-app-js.sh: -------------------------------------------------------------------------------- 1 | shadow release app 2 | 3 | # jar tf target/crux-console.jar | grep main.js 4 | # zip -d target/crux-console.jar static/crux-ui/compiled/main.js 5 | 6 | jar uf target/crux-console.jar -C resources static/crux-ui/compiled/main.js 7 | # says: 8 | # jar update file name.jar -Cd to resources and put path/main.js into it 9 | -------------------------------------------------------------------------------- /doc/README.adoc: -------------------------------------------------------------------------------- 1 | = Crux Console docs 2 | 3 | This directory represents the Crux Console documentation module. 4 | 5 | It is explicitly referenced by `app`. 6 | 7 | Asciidoc sources can be found in link:resources/doc/sources[]. 8 | -------------------------------------------------------------------------------- /doc/resources/doc/asciidoctor/attributes.edn: -------------------------------------------------------------------------------- 1 | {"docinfo" "shared" 2 | "docinfodir" "doc" 3 | "experimental" true 4 | "figure-caption" false 5 | "highlightjs-theme" "tomorrow-night" 6 | "highlightjsdir" "/highlight" 7 | "icons" "font" 8 | "iconfont-cdn" "/font-awesome/css/font-awesome.min.css" 9 | "icontype" "svg" 10 | "imagesdir" "/img" 11 | "nofooter" true 12 | "sectnums" true 13 | "sectanchors" true 14 | "source-highlighter" "highlightjs" 15 | "stylesdir" "/doc/css" 16 | "stylesheet" "juxt.css" 17 | "linkcss" true 18 | "toc" "left" 19 | "webfonts" false 20 | "xrefstyle" "short"} 21 | -------------------------------------------------------------------------------- /doc/resources/doc/docinfo/docinfo-footer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 |

Copyright © 2019, JUXT LTD. All Rights Reserved.

8 |
9 | 10 |
11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /doc/resources/doc/docinfo/docinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /doc/resources/img/console1-runqempty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/doc/resources/img/console1-runqempty.gif -------------------------------------------------------------------------------- /ebs/.ebextensions/https-instance-securitygroup.config: -------------------------------------------------------------------------------- 1 | Resources: 2 | sslSecurityGroupIngress: 3 | Type: AWS::EC2::SecurityGroupIngress 4 | Properties: 5 | GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]} 6 | IpProtocol: tcp 7 | ToPort: 443 8 | FromPort: 443 9 | CidrIp: 0.0.0.0/0 10 | -------------------------------------------------------------------------------- /ebs/.ebextensions/nginx/conf.d/console.conf: -------------------------------------------------------------------------------- 1 | # See guide 2 | # https://geekflare.com/nginx-webserver-security-hardening-guide/ 3 | 4 | # redirect all http traffic to https 5 | server { 6 | listen 80 default_server; 7 | listen [::]:80 default_server; 8 | server_name .console.crux.cloud; 9 | return 301 https://$host$request_uri; 10 | } 11 | 12 | server { 13 | listen 443 ssl http2; 14 | listen [::]:443 ssl http2; 15 | server_name .console.crux.cloud; 16 | 17 | ssl_certificate /etc/letsencrypt/live/console.crux.cloud/fullchain.pem; # managed by Certbot 18 | ssl_certificate_key /etc/letsencrypt/live/console.crux.cloud/privkey.pem; # managed by Certbot 19 | 20 | # enable session resumption to improve https performance 21 | # http://vincent.bernat.im/en/blog/2011-ssl-session-reuse-rfc5077.html 22 | ssl_session_cache shared:SSL:50m; 23 | ssl_session_timeout 1d; 24 | ssl_session_tickets off; 25 | 26 | # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits 27 | # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 28 | # not worth the time to produce it on EBS instances 29 | 30 | # enable ocsp stapling (mechanism by which a site can convey certificate revocation information to visitors in a privacy-preserving, scalable manner) 31 | # http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox/ 32 | resolver 8.8.8.8 8.8.4.4; 33 | ssl_stapling on; 34 | ssl_stapling_verify on; 35 | 36 | 37 | # config to don't allow the browser to render the page inside an frame or iframe 38 | # and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking 39 | # if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri 40 | # https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options 41 | # add_header X-Frame-Options SAMEORIGIN; 42 | # comes with aleph 43 | 44 | # when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header, 45 | # to disable content-type sniffing on some browsers. 46 | # https://www.owasp.org/index.php/List_of_useful_HTTP_headers 47 | # currently suppoorted in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx 48 | # http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx 49 | # 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020 50 | # add_header X-Content-Type-Options nosniff; 51 | # comes with aleph 52 | 53 | # This header enables the Cross-site scripting (XSS) filter built into most recent web browsers. 54 | # It's usually enabled by default anyway, so the role of this header is to re-enable the filter for 55 | # this particular website if it was disabled by the user. 56 | # https://www.owasp.org/index.php/List_of_useful_HTTP_headers 57 | # add_header X-XSS-Protection "1; mode=block"; 58 | # comes with aleph 59 | 60 | # with Content Security Policy (CSP) enabled(and a browser that supports it(http://caniuse.com/#feat=contentsecuritypolicy), 61 | # you can tell the browser that it can only download content from the domains you explicitly allow 62 | # http://www.html5rocks.com/en/tutorials/security/content-security-policy/ 63 | # https://www.owasp.org/index.php/Content_Security_Policy 64 | # I need to change our application code so we can increase security by disabling 'unsafe-inline' 'unsafe-eval' 65 | # directives for css and js(if you have inline css or js, you will need to keep it too). 66 | # more: http://www.html5rocks.com/en/tutorials/security/content-security-policy/#inline-code-considered-harmful 67 | add_header Content-Security-Policy 68 | "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; img-src 'self' https://juxt.pro; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://themes.googleusercontent.com; frame-src 'none'; object-src 'none'; connect-src 'self' https://gist.githubusercontent.com/"; 69 | 70 | 71 | # config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security 72 | # to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping 73 | # also https://hstspreload.org/ 74 | add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"; 75 | 76 | client_max_body_size 2M; 77 | # ... the rest of your configuration 78 | 79 | root /srv/crux/actual/public; 80 | 81 | if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) 82 | { 83 | return 405; 84 | } 85 | 86 | location /crux/ { 87 | proxy_pass http://127.0.0.1:8080/; 88 | # proxy_set_header Connection $connection_upgrade; 89 | # proxy_set_header Upgrade $http_upgrade; 90 | # proxy_set_header Host $host; 91 | # proxy_set_header X-Real-IP $remote_addr; 92 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 93 | } 94 | 95 | location / { 96 | proxy_pass http://localhost:5000/; # important for bidi 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ebs/.ebextensions/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | #au BufWritePost * !service nginx reload 2 | #au BufWritePost * !service nginx restart 3 | user nginx; 4 | worker_processes auto; 5 | pid /var/run/nginx.pid; 6 | worker_rlimit_nofile 32794; 7 | error_log /var/log/nginx/error.log warn; 8 | 9 | events { 10 | worker_connections 1024; 11 | # multi_accept on; 12 | } 13 | 14 | http { 15 | 16 | ## 17 | # Basic Settings 18 | ## 19 | 20 | sendfile on; 21 | tcp_nopush on; 22 | tcp_nodelay on; 23 | keepalive_timeout 65; 24 | types_hash_max_size 2048; 25 | 26 | # server_names_hash_bucket_size 64; 27 | # server_name_in_redirect off; 28 | 29 | include /etc/nginx/mime.types; 30 | default_type application/octet-stream; 31 | 32 | ## 33 | # Logging Settings 34 | ## 35 | 36 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 37 | '$status $body_bytes_sent "$http_referer" ' 38 | '"$http_user_agent" "$http_x_forwarded_for"'; 39 | 40 | ## 41 | # SSL settings 42 | ## 43 | 44 | # ciphers chosen for forward secrecy and compatibility 45 | # http://blog.ivanristic.com/2013/08/configuring-apache-nginx-and-openssl-for-forward-secrecy.html 46 | ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA HIGH !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"; 47 | 48 | # enables server-side protection from BEAST attacks 49 | # http://blog.ivanristic.com/2013/09/is-beast-still-a-threat.html 50 | ssl_prefer_server_ciphers on; 51 | 52 | # disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS http://en.wikipedia.org/wiki/Secure_Sockets_Layer#SSL_3.0 53 | ssl_protocols TLSv1.2 TLSv1.3; 54 | 55 | 56 | # read more here http://tautt.com/best-nginx-configuration-for-security/ 57 | # don't send the nginx version number in error pages and Server header 58 | server_tokens off; 59 | 60 | access_log /var/log/nginx/access.log main; 61 | 62 | # Gzip Settings 63 | gzip on; 64 | gzip_static on; 65 | gzip_proxied any; 66 | gzip_comp_level 9; 67 | gzip_types text/plain text/css application/json application/edn application/javascript text/xml application/xml application/xml+rss text/javascript; 68 | 69 | # Virtual Host Configs 70 | include conf.d/console.conf; 71 | #include conf.d/elasticbeanstalk/*; 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /ebs/generate-dh.sh: -------------------------------------------------------------------------------- 1 | # to generate your dhparam.pem file, run in the terminal 2 | openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096 3 | -------------------------------------------------------------------------------- /ebs/readme.md: -------------------------------------------------------------------------------- 1 | https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/java-se-nginx.html 2 | -------------------------------------------------------------------------------- /ebs/sample-console-install.config: -------------------------------------------------------------------------------- 1 | # If you need custom software on your EBS instance 2 | # read this 3 | # https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html 4 | # then drop this config into .ebextensions dir 5 | 6 | #commands: 7 | # 10_installcert: 8 | # command: "wget https://dl.eff.org/certbot-auto;chmod a+x certbot-auto" 9 | # 13_getcert: 10 | # command: "sudo ./certbot-auto certonly --debug --non-interactive --email jdt@juxt.pro --agree-tos --standalone --domains console.crux.cloud --keep-until-expiring --pre-hook \"service nginx stop\" --staging" 11 | # 31-hey: 12 | # command: echo "kitkattoken" && pwd 13 | # 32-hey: 14 | # command: wget -q https://download.java.net/java/GA/jdk13/5b8a42f3905b406298b72d750b6919f6/33/GPL/openjdk-13_linux-x64_bin.tar.gz 15 | # 33-hey: 16 | # command: tar -xzf openjdk-13_linux-x64_bin.tar.gz 17 | # 34-hey: 18 | # command: export JAVA_HOME=$(pwd)/jdk-13 19 | # 35-hey: 20 | # command: export PATH=$(pwd)/jdk-13/bin:$PATH 21 | 22 | container_commands: 23 | 31-hey: 24 | command: echo "containertoken" && pwd 25 | -------------------------------------------------------------------------------- /ebs/stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | Description: 4 | Type: String 5 | Description: Description of your application for Elastic Beanstalk Resources 6 | Default: "Crux Console" 7 | CNAMEPrefix: 8 | Type: String 9 | Description: Prefix to be used in the generated CNAME 10 | Resources: 11 | sslSecurityGroupIngress: 12 | Type: AWS::EC2::SecurityGroupIngress 13 | Properties: 14 | GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]} 15 | IpProtocol: tcp 16 | ToPort: 443 17 | FromPort: 443 18 | CidrIp: 0.0.0.0/0 19 | BeanstalkInstanceRole: 20 | Type: "AWS::IAM::Role" 21 | Properties: 22 | AssumeRolePolicyDocument: 23 | Version: 2012-10-17 24 | Statement: 25 | - 26 | Effect: Allow 27 | Principal: 28 | Service: 29 | - ec2.amazonaws.com 30 | Action: 31 | - sts:AssumeRole 32 | ManagedPolicyArns: 33 | - arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier 34 | - arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker 35 | BeanstalkInstanceProfile: 36 | Type: AWS::IAM::InstanceProfile 37 | Properties: 38 | Roles: 39 | - !Ref BeanstalkInstanceRole 40 | Application: 41 | Type: AWS::ElasticBeanstalk::Application 42 | Properties: 43 | Description: !Ref Description 44 | ConfigurationTemplate: 45 | Type: AWS::ElasticBeanstalk::ConfigurationTemplate 46 | Properties: 47 | ApplicationName: !Ref Application 48 | Description: !Join [" ", [!Ref Description, "Configuration"]] 49 | OptionSettings: 50 | - Namespace: aws:autoscaling:launchconfiguration 51 | OptionName: IamInstanceProfile 52 | Value: !Ref BeanstalkInstanceProfile 53 | - Namespace: aws:autoscaling:asg 54 | OptionName: MinSize 55 | Value: '1' 56 | - Namespace: aws:autoscaling:asg 57 | OptionName: MaxSize 58 | Value: '1' 59 | - Namespace: aws:elasticbeanstalk:environment 60 | OptionName: EnvironmentType 61 | Value: LoadBalanced 62 | - Namespace: aws:elasticbeanstalk:cloudwatch:logs 63 | OptionName: StreamLogs 64 | Value: true 65 | SolutionStackName: 64bit Amazon Linux 2018.03 v2.8.2 running Java 8 66 | Environment: 67 | Type: AWS::ElasticBeanstalk::Environment 68 | Properties: 69 | ApplicationName: !Ref Application 70 | Description: !Join [" ", [!Ref Description, "Environment"]] 71 | CNAMEPrefix: !Ref CNAMEPrefix 72 | TemplateName: !Ref ConfigurationTemplate 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crux-ui", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Jeremy Taylor [refset]", 6 | "Ivan Fedorov [spacegangster]" 7 | ], 8 | "private": true, 9 | "dependencies": { 10 | "codemirror": "5.47.0", 11 | "create-react-class": "^15.6.3", 12 | "highlight.js": "9.15.8", 13 | "plotly.js": "^1.49.3", 14 | "plotly.js-gl3d-dist": "^1.49.3", 15 | "process": "^0.11.10", 16 | "quill": ">=1.3.6", 17 | "quill-auto-links": ">=0.1.3", 18 | "quill-delta": ">=4.1.0", 19 | "quill-plain-text-paste": ">=1.0.1", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.7.0", 22 | "react-flip-move": "^3.0.3", 23 | "react-highlight.js": "^1.0.7", 24 | "react-input-range": "^1.3.0", 25 | "react-ui-tree": "^4.0.0" 26 | }, 27 | "resolutions": { 28 | "esm": "^3.1.0", 29 | "static-eval": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "shadow-cljs": "^2.6.24" 33 | }, 34 | "peerDependencies": { 35 | "gl-surface3d": "^1.4.6", 36 | "graceful-fs": "^4.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject crux-console "derived-from-git" 2 | :dependencies 3 | [[org.clojure/clojure "1.10.1"] 4 | ; 1.10.1 doesn't work with this aleph 5 | [aleph "0.4.6"] 6 | [bidi "2.1.6"] 7 | [hiccup "1.0.5"] 8 | [page-renderer "0.4.4"]] 9 | 10 | :min-lein-version "2.9.1" 11 | :main crux-ui-server.main 12 | :aot [crux-ui-server.main] 13 | :uberjar-name "crux-console-skimmed.jar" 14 | :omit-sources true 15 | :resource-paths ["resources"] 16 | 17 | :profiles 18 | {:dev 19 | {:main dev :repl-options {:init-ns dev} 20 | :source-paths ["src" "test" #_"node_modules"] 21 | :dependencies [[nrepl/nrepl "0.6.0"]]} 22 | :crux-jars 23 | {:uberjar-name "crux-console.jar" 24 | :auto-clean false 25 | :dependencies 26 | [[juxt/crux-core "20.01-1.6.2-alpha"] 27 | [juxt/crux-rocksdb "20.01-1.6.2-alpha"] 28 | [juxt/crux-http-server "20.01-1.6.2-alpha"]]} 29 | :shadow-cljs ; also see package.json deps 30 | {:dependencies 31 | [[org.clojure/clojurescript "1.10.520"] 32 | [reagent "0.8.1"] 33 | [re-frame "0.10.8"] 34 | [garden "1.3.9"] 35 | [medley "1.2.0"] 36 | [funcool/promesa "2.0.1"] 37 | [org.clojure/tools.logging "0.5.0"] 38 | [com.andrewmcveigh/cljs-time "0.5.2"] 39 | [binaryage/oops "0.6.4"] 40 | [day8.re-frame/test "0.1.5"] 41 | [day8.re-frame/re-frame-10x "0.3.3"] 42 | [thheller/shadow-cljs "2.8.52"] 43 | [com.google.javascript/closure-compiler-unshaded "v20190819"] 44 | [org.clojure/google-closure-library "0.0-20190213-2033d5d9"]]}} 45 | 46 | ; AWS is using Java 8 47 | :javac-options ["-source" "8" "-target" "8" 48 | "-XDignore.symbol.file" 49 | "-Xlint:all,-options,-path" 50 | "-Werror" 51 | "-proc:none"] 52 | 53 | :repositories [["clojars" "https://repo.clojars.org"]] 54 | :plugins [;[lein-shadow "0.1.5"] ; nasty guy, deletes original shadow-cljs config if you run it 55 | [lein-shell "0.5.0"]] ; https://github.com/hypirion/lein-shell 56 | 57 | :clean-targets ^{:protect false} ["target" #_"resources/static/crux-ui/compiled"] 58 | 59 | :aliases 60 | {"yarn" 61 | ["do" ["shell" "yarn" "install"]] 62 | 63 | "shadow" 64 | ["shell" "node_modules/.bin/shadow-cljs"] 65 | 66 | "build" 67 | ["do" 68 | ["clean"] 69 | ["yarn"] 70 | ["shadow" "release" "app"] ; compile 71 | #_["shadow" "release" "app-perf"]] ; compile production ready performance charts app 72 | 73 | "ebs" 74 | ["do" ["shell" "sh" "./dev/build-ebs.sh"]] 75 | 76 | "build-ebs" 77 | ["do" 78 | ["build"] 79 | ["uberjar"] 80 | ["ebs"]] 81 | 82 | "cljs-dev" 83 | ["do" 84 | ["yarn"] 85 | ["shadow" "watch" "app"]] 86 | 87 | "dep-tree" 88 | ["do" 89 | ["yarn"] 90 | ["shadow" "pom"] 91 | ["shell" "mvn" "dependency:tree"]] 92 | 93 | "build-report" 94 | [["yarn"] 95 | ["shadow" "run" "shadow.cljs.build-report" "app" "report.html"]]}) 96 | 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Crux Console 2 | 3 | Console is a web app that allows you to query Crux and see data from different perpectives. 4 | ![Screenshot of Crux Console](./resources/screenshot-1.png) 5 | 6 | It can be used as a standalone analytics tool or as an educational tool. 7 | 8 | ## Educational use 9 | Console comes with a set of example queries but you can make it load your own 10 | set by providing a url param `examples-gist=gist-raw-url`, like 11 | [here](http://console.crux.cloud/console?examples-gist=https://gist.githubusercontent.com/spacegangster/b68f72e3c81524a71af1f3033ea7507e/raw/5fb55fe8e766245a3338f6e5c508ffbbe824900f/examples.edn) 12 | This way Console will import this file. 13 | Important note : link must point to raw gist content. 14 | Preset queries example: 15 | ```clojure 16 | [{:title "Space Stocks" 17 | :query {:find [e] 18 | :where [[e :crux.db/id _]]}} 19 | {:title "Add space stocks" 20 | :query [[:crux.tx/put {:crux.db/id 33 :title "Company 3"}]]}] 21 | ``` 22 | 23 | ## Deployment scenarios 24 | Can be used as an analytics tool 25 | For nginx – the following redirects may be used 26 | 27 | ``` 28 | # in a server block 29 | location /console { 30 | # don't forget about at least basic auth 31 | proxy_pass http://localhost:5000; 32 | } 33 | location /crux { 34 | rewrite ^(/crux)(/.*)$ $2 break; 35 | proxy_pass http://localhost:4889/; 36 | } 37 | ``` 38 | 39 | ### Command line flags 40 | ``` 41 | --frontend-port 5000 42 | --embed-crux false 43 | --conf-file crux-console-conf.edn 44 | --crux-http-port 8080 45 | ``` 46 | 47 | ## How to build or develop 48 | 49 | ### Prerequisites 50 | 51 | All builds require `node` and `yarn` to be installed. 52 | 53 | `yarn` is, at the moment, a much better alternative to npm. 54 | Yarn ROI is that in 2-3 uses it will save more time than you spent 55 | installing and learning it, compared to 2-3 runs of npm install. 56 | 57 | 58 | ### Release flow 59 | 1. `lein build` will produce you production ready assets 60 | 2. `lein build-ebs` does the build above and packs it into 61 | Elastic Beanstalk package which we upload to AWS to see console.crux.cloud 62 | 63 | 64 | ### Dev flow 65 | To launch development REPL 66 | 67 | #### Preferred 68 | ```sh 69 | lein cljs-dev # will install all the node modules, launch cljs compiling guard with code hotswapping 70 | lein repl # will launch edge rebel repl 71 | ``` 72 | 73 | 74 | #### Alternative 75 | ```sh 76 | # once 77 | yarn install 78 | 79 | dev/shadow-dev & 80 | # will launch shadow-cljs watch build 81 | # it runs a local version 82 | 83 | lein repl 84 | # will launch the server 85 | ``` 86 | 87 | 88 | 89 | ## Sidenotes 90 | 91 | #### Plotly 92 | Note that Plotly has its own packaging model (due to heavy weight beneath), 93 | so search for different packages on npm if you need more fine-grained control. 94 | e.g. 95 | 96 | - All-in-one https://www.npmjs.com/package/plotly.js-dist 97 | 98 | ### Datepickers 99 | There are a few things we can look at 100 | - Pikaday 101 | - looking nice and simple 102 | - no deps 103 | - url https://github.com/timgilbert/cljs-pikaday 104 | - no timepicker, but that's an easier fix 105 | 106 | - React datetime picker 107 | - demo url http://projects.wojtekmaj.pl/react-datetime-picker/ 108 | - gh url: https://github.com/wojtekmaj/react-datetime-picker 109 | - good UX, has timepicker 110 | - claimed test cov 92% 111 | - seemingly lots of deps :( 112 | 113 | - Hacker one datepicker 114 | - demo link: https://reactdatepicker.com 115 | - gh link: https://github.com/Hacker0x01/react-datepicker/ 116 | - Lots of options, arguable best datepicker-timepicker combination 117 | - quite heavy :( 118 | 119 | Things to be aware of 120 | - YouCanBookMe/react-datetime 121 | - drags moment.js as a dep 122 | - UX is questionable 123 | 124 | ## Potential extension points 125 | 126 | ### Build 127 | Can be improved with skipping with uberjar build and just updating the jar with 128 | 129 | ``` 130 | jar uf jar-file input-file(s) 131 | ``` 132 | -------------------------------------------------------------------------------- /resources/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/resources/screenshot-1.png -------------------------------------------------------------------------------- /resources/static/img/console-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 49 | 56 | 57 | 60 | 68 | 73 | 76 | 79 | 81 | 89 | 94 | 95 | 96 | [?console] 111 | 121 | -------------------------------------------------------------------------------- /resources/static/img/crux-labs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/resources/static/img/crux-labs.png -------------------------------------------------------------------------------- /resources/static/img/crux-labs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/static/img/crux-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 16 | 20 | 25 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /resources/static/img/cube-for-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/static/img/cube-on-white-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/resources/static/img/cube-on-white-120.png -------------------------------------------------------------------------------- /resources/static/img/cube-on-white-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/resources/static/img/cube-on-white-192.png -------------------------------------------------------------------------------- /resources/static/img/cube-on-white-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtdb-labs/crux-console/6bdc59339dd2513f60b61188c1c2f331006511fd/resources/static/img/cube-on-white-512.png -------------------------------------------------------------------------------- /resources/static/img/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/static/img/juxt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/static/styles/eclipse.css: -------------------------------------------------------------------------------- 1 | .cm-s-eclipse span.cm-meta { color: #FF1717; } 2 | .cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } 3 | .cm-s-eclipse span.cm-atom { color: #219; } 4 | .cm-s-eclipse span.cm-number { color: #164; } 5 | .cm-s-eclipse span.cm-def { color: #00f; } 6 | .cm-s-eclipse span.cm-variable { color: black; } 7 | .cm-s-eclipse span.cm-variable-2 { color: #0000C0; } 8 | .cm-s-eclipse span.cm-variable-3, .cm-s-eclipse span.cm-type { color: #0000C0; } 9 | .cm-s-eclipse span.cm-property { color: black; } 10 | .cm-s-eclipse span.cm-operator { color: black; } 11 | .cm-s-eclipse span.cm-comment { color: #3F7F5F; } 12 | .cm-s-eclipse span.cm-string { color: #2A00FF; } 13 | .cm-s-eclipse span.cm-string-2 { color: #f50; } 14 | .cm-s-eclipse span.cm-qualifier { color: #555; } 15 | .cm-s-eclipse span.cm-builtin { color: #30a; } 16 | .cm-s-eclipse span.cm-bracket { color: #cc7; } 17 | .cm-s-eclipse span.cm-tag { color: #170; } 18 | .cm-s-eclipse span.cm-attribute { color: #00c; } 19 | .cm-s-eclipse span.cm-link { color: #219; } 20 | .cm-s-eclipse span.cm-error { color: #f00; } 21 | 22 | .cm-s-eclipse .CodeMirror-activeline-background { background: #e8f2ff; } 23 | .cm-s-eclipse .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; } 24 | -------------------------------------------------------------------------------- /resources/static/styles/monokai.css: -------------------------------------------------------------------------------- 1 | /* Based on Sublime Text's Monokai theme */ 2 | 3 | .cm-s-monokai.CodeMirror { background: #272822; color: #f8f8f2; } 4 | .cm-s-monokai div.CodeMirror-selected { background: #49483E; } 5 | .cm-s-monokai .CodeMirror-line::selection, .cm-s-monokai .CodeMirror-line > span::selection, .cm-s-monokai .CodeMirror-line > span > span::selection { background: rgba(73, 72, 62, .99); } 6 | .cm-s-monokai .CodeMirror-line::-moz-selection, .cm-s-monokai .CodeMirror-line > span::-moz-selection, .cm-s-monokai .CodeMirror-line > span > span::-moz-selection { background: rgba(73, 72, 62, .99); } 7 | .cm-s-monokai .CodeMirror-gutters { background: #272822; border-right: 0px; } 8 | .cm-s-monokai .CodeMirror-guttermarker { color: white; } 9 | .cm-s-monokai .CodeMirror-guttermarker-subtle { color: #d0d0d0; } 10 | .cm-s-monokai .CodeMirror-linenumber { color: #d0d0d0; } 11 | .cm-s-monokai .CodeMirror-cursor { border-left: 1px solid #f8f8f0; } 12 | 13 | .cm-s-monokai span.cm-comment { color: #75715e; } 14 | .cm-s-monokai span.cm-atom { color: #ae81ff; } 15 | .cm-s-monokai span.cm-number { color: #ae81ff; } 16 | 17 | .cm-s-monokai span.cm-comment.cm-attribute { color: #97b757; } 18 | .cm-s-monokai span.cm-comment.cm-def { color: #bc9262; } 19 | .cm-s-monokai span.cm-comment.cm-tag { color: #bc6283; } 20 | .cm-s-monokai span.cm-comment.cm-type { color: #5998a6; } 21 | 22 | .cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { color: #a6e22e; } 23 | .cm-s-monokai span.cm-keyword { color: #f92672; } 24 | .cm-s-monokai span.cm-builtin { color: #66d9ef; } 25 | .cm-s-monokai span.cm-string { color: #e6db74; } 26 | 27 | .cm-s-monokai span.cm-variable { color: #f8f8f2; } 28 | .cm-s-monokai span.cm-variable-2 { color: #9effff; } 29 | .cm-s-monokai span.cm-variable-3, .cm-s-monokai span.cm-type { color: #66d9ef; } 30 | .cm-s-monokai span.cm-def { color: #fd971f; } 31 | .cm-s-monokai span.cm-bracket { color: #f8f8f2; } 32 | .cm-s-monokai span.cm-tag { color: #f92672; } 33 | .cm-s-monokai span.cm-header { color: #ae81ff; } 34 | .cm-s-monokai span.cm-link { color: #ae81ff; } 35 | .cm-s-monokai span.cm-error { background: #f92672; color: #f8f8f0; } 36 | 37 | .cm-s-monokai .CodeMirror-activeline-background { background: #373831; } 38 | .cm-s-monokai .CodeMirror-matchingbracket { 39 | text-decoration: underline; 40 | color: white !important; 41 | } 42 | -------------------------------------------------------------------------------- /resources/static/styles/react-input-range.css: -------------------------------------------------------------------------------- 1 | .input-range__slider { 2 | -webkit-appearance: none; 3 | -moz-appearance: none; 4 | appearance: none; 5 | background: #3f51b5; 6 | border: 1px solid #3f51b5; 7 | border-radius: 100%; 8 | cursor: pointer; 9 | display: block; 10 | height: 1rem; 11 | margin-left: -0.5rem; 12 | margin-top: -0.65rem; 13 | outline: none; 14 | position: absolute; 15 | top: 0%; 16 | transition: transform 0.1s ease-out, box-shadow 0.1s ease-out; 17 | will-change: transform, box-shadow; 18 | width: 1rem; } 19 | .input-range__slider:active { 20 | transform: scale(1.3); } 21 | .input-range__slider:focus { 22 | box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); } 23 | .input-range--disabled .input-range__slider { 24 | background: #cccccc; 25 | border: 1px solid #cccccc; 26 | box-shadow: none; 27 | transform: none; } 28 | 29 | .input-range__slider-container { 30 | transition: left 0.3s ease-out; } 31 | 32 | 33 | .input-range__label { 34 | color: #aaaaaa; 35 | font-size: 0.8rem; 36 | transform: translateZ(0); 37 | white-space: nowrap; } 38 | 39 | .input-range__label--min, 40 | .input-range__label--max { 41 | display: none 42 | } 43 | 44 | .input-range__label--min { 45 | grid-area: l; 46 | left: 0; } 47 | 48 | .input-range__label--max { 49 | grid-area: r; 50 | right: 0; } 51 | 52 | .input-range__label--value, 53 | .input-range__label--value-year { 54 | top: -0.92em; 55 | position: absolute; 56 | left: 1em; 57 | text-align: right; 58 | width: 1.8em; 59 | color: black; 60 | font-size: 1.0em; 61 | font-weight: 400; 62 | font-variant-numeric: tabular-nums;} 63 | 64 | .input-range__label--value-year { 65 | left: 1.7em 66 | } 67 | 68 | .input-range__label-container { 69 | left: -50%; 70 | position: relative; } 71 | .input-range__label--max .input-range__label-container { 72 | left: 50%; } 73 | 74 | .input-range__track { 75 | background: #eeeeee; 76 | border-radius: 0.3rem; 77 | cursor: pointer; 78 | display: block; 79 | grid-area: c; 80 | height: 0.3rem; 81 | position: relative; 82 | will-change: left, width; 83 | transition: left 0.1s ease-out, width 0.1s ease-out; } 84 | .input-range--disabled .input-range__track { 85 | background: #eeeeee; } 86 | 87 | .input-range__track--background { 88 | left: 0; 89 | margin-top: -0.15rem; 90 | position: absolute; 91 | right: 0; 92 | top: 50%; } 93 | 94 | .input-range__track--active { 95 | background: #3f51b5; } 96 | 97 | .input-range { 98 | height: 1rem; 99 | position: relative; 100 | display: grid; 101 | grid-template: 'l c r' / 0px 1fr 30px; 102 | width: 100%; } 103 | -------------------------------------------------------------------------------- /resources/static/styles/react-ui-tree.css: -------------------------------------------------------------------------------- 1 | .f-no-select { 2 | -webkit-user-select: none; 3 | -khtml-user-select: none; 4 | -moz-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | .m-tree { 9 | position: relative; 10 | overflow: hidden; 11 | -webkit-user-select: none; 12 | -khtml-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | } 17 | .m-draggable { 18 | position: absolute; 19 | opacity: 0.8; 20 | -webkit-user-select: none; 21 | -khtml-user-select: none; 22 | -moz-user-select: none; 23 | -ms-user-select: none; 24 | user-select: none; 25 | } 26 | .m-node.placeholder > * { 27 | visibility: hidden; 28 | } 29 | .m-node.placeholder { 30 | border: 1px dashed #ccc; 31 | } 32 | .m-node .inner { 33 | position: relative; 34 | cursor: pointer; 35 | padding-left: 10px; 36 | } 37 | .m-node .collapse { 38 | position: absolute; 39 | left: 0; 40 | cursor: pointer; 41 | } 42 | .m-node .caret-right:before { 43 | content: '\25B8'; 44 | } 45 | .m-node .caret-down:before { 46 | content: '\25BE'; 47 | } 48 | -------------------------------------------------------------------------------- /resources/static/styles/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, button, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | box-sizing: border-box; 21 | } 22 | 23 | 24 | h1 { 25 | font-size: 36px 26 | } 27 | h2 { 28 | font-size: 30px 29 | } 30 | h3 { 31 | font-size: 24px 32 | } 33 | h4 { 34 | font-size: 18px 35 | } 36 | h5 { 37 | font-size: 14px 38 | } 39 | h6 { 40 | font-size: 12px 41 | } 42 | 43 | body { 44 | line-height: 1.5; 45 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 46 | } 47 | 48 | table { 49 | border-collapse: collapse; 50 | border-spacing: 0; 51 | } 52 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths ["src" "test" "node_modules"] 3 | 4 | :dependencies 5 | [[reagent "0.8.1"] 6 | [re-frame "0.10.8"] 7 | [garden "1.3.9"] 8 | [bidi "2.1.6"] 9 | [stylefy "1.13.3"] 10 | [medley "1.2.0"] 11 | [day8.re-frame/re-frame-10x "0.4.1"] 12 | [clj-commons/secretary "1.2.4"] 13 | [funcool/promesa "2.0.1"] 14 | [com.andrewmcveigh/cljs-time "0.5.2"] 15 | [binaryage/oops "0.6.4"]] 16 | 17 | :builds 18 | {:app 19 | {:target :browser 20 | :modules {:main {:init-fn crux-ui.main/init}} 21 | :output-dir "resources/static/crux-ui/compiled" 22 | :compiler-options {:optimizations :advanced} 23 | :asset-path "/console/static/crux-ui/compiled"} 24 | 25 | :app-perf 26 | {:target :browser 27 | :modules {:main-perf {:init-fn crux-ui.main-perf/init}} 28 | :output-dir "resources/static/crux-ui/compiled" 29 | :compiler-options {:optimizations :advanced} 30 | :asset-path "/console/static/crux-ui/compiled"} 31 | 32 | 33 | :test 34 | {:target :browser-test 35 | :test-dir "resources/static/crux-ui/test" 36 | :ns-regexp "-test$" 37 | :runner-ns crux-console.test-runner 38 | :devtools 39 | {:http-port 4001 40 | :http-root "resources/static/crux-ui/test"}} 41 | 42 | :dev 43 | {:target :browser 44 | :modules {:main {:init-fn crux-ui.main/init}} 45 | :output-dir "resources/static/crux-ui/compiled" 46 | :asset-path "/console/static/crux-ui/compiled" 47 | :compiler-options {:closure-warnings {:global-this :off} 48 | :closure-defines {re-frame.trace.trace-enabled? true} 49 | :optimizations :none} 50 | :devtools 51 | {:after-load crux-ui.main/reload! 52 | :preloads [day8.re-frame-10x.preload]}} 53 | 54 | :dev-perf 55 | {:target :browser 56 | :modules 57 | {:main-perf {:init-fn crux-ui.main-perf/init}} 58 | :output-dir "resources/static/crux-ui/compiled" 59 | :asset-path "/static/crux-ui/compiled" 60 | :compiler-options {:closure-warnings {:global-this :off} 61 | :closure-defines {re-frame.trace.trace-enabled? true} 62 | :optimizations :none} 63 | :devtools 64 | {:after-load crux-ui.main-perf/reload! 65 | :preloads [day8.re-frame-10x.preload]}}} 66 | 67 | :nrepl {:port 55300 68 | :init-ns crux-ui.main}} 69 | -------------------------------------------------------------------------------- /src/crux_ui/better_printer.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.better-printer 2 | (:require [cljs.pprint :as pp] 3 | [clojure.string :as s])) 4 | 5 | 6 | (defmulti better-printer 7 | (fn [edn] 8 | (cond 9 | (string? edn) ::string 10 | (map? edn) ::map 11 | (vector? edn) ::vec 12 | :else ::default))) 13 | 14 | (defn simple-print [edn] 15 | (with-out-str (pp/pprint edn))) 16 | 17 | (defn- print-entry [i [k v]] 18 | (if (= 0 i) 19 | (str (pr-str k) "\t" (pr-str v)) 20 | (str " " (pr-str k) "\t" (pr-str v)))) 21 | 22 | (defmethod better-printer ::map [edn] 23 | (str "{" (s/join "\n" (map-indexed print-entry edn)) "}")) 24 | 25 | (defmethod better-printer ::string [edn-str] 26 | (pr-str edn-str)) 27 | 28 | (defmethod better-printer ::vec [edn] 29 | (with-out-str (pp/pprint edn))) 30 | 31 | (defmethod better-printer ::default [edn] 32 | (with-out-str (pp/pprint edn))) 33 | 34 | (comment 35 | (println 36 | (better-printer '{:find [e p], :where [[e :crux.db/id _] [e :ticker/price p]]})) 37 | (println 38 | (better-printer '[:find [e p], :where [[e :crux.db/id _] [e :ticker/price p]]]))) 39 | -------------------------------------------------------------------------------- /src/crux_ui/config.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.config 2 | (:require ["./ua-regexp.js" :as ua-rgx])) 3 | 4 | (def url-docs "https://juxt.pro/crux/docs/index.html") 5 | (def url-chat "https://juxt-oss.zulipchat.com/#narrow/stream/194466-crux") 6 | (def url-mail "crux@juxt.pro") 7 | (def url-examples-gist "https://gist.githubusercontent.com/spacegangster/b68f72e3c81524a71af1f3033ea7507e/raw/572396dec0791500c965fea443b2f26a60f500d4/examples.edn") 8 | 9 | (def ^:const ua-regex ua-rgx) 10 | 11 | (def ^:const user-agent js/navigator.userAgent) 12 | 13 | (def ^:const ua-info 14 | (re-find ua-regex user-agent)) 15 | 16 | (def ^:const browser-vendor-string (second ua-info)) 17 | (def ^:const browser-version-string (nth ua-info 2)) 18 | 19 | (def ^:const browser-vendor 20 | (case browser-vendor-string 21 | "Chrome" :browser/chrome 22 | "Firefox" :browser/firefox 23 | "Opera" :browser/opera 24 | "Safari" :browser/safari 25 | "Edge" :browser/edge 26 | :browser/unknown)) 27 | 28 | (def ^:const supports-input-datetime? 29 | (not (#{:browser/firefox :browser/ie :browser/safari :browser/unknown} browser-vendor))) 30 | -------------------------------------------------------------------------------- /src/crux_ui/cookies.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.cookies 2 | (:refer-clojure :exclude [count get keys vals empty? reset!]) 3 | (:require [goog.net.cookies :as cks] 4 | [cljs.reader :as reader])) 5 | 6 | (defn set! 7 | "sets a cookie, the max-age for session cookie 8 | following optional parameters may be passed in as a map: 9 | :max-age - defaults to -1 10 | :path - path of the cookie, defaults to the full request path 11 | :domain - domain of the cookie, when null the browser will use the full request host name 12 | :secure? - boolean specifying whether the cookie should only be sent over a secure channel 13 | :raw? - boolean specifying whether content should be stored raw, or as EDN 14 | " 15 | [k content & [{:keys [max-age path domain secure? raw?]} :as opts]] 16 | (let [k (name k) 17 | content (if raw? 18 | (str content) 19 | (pr-str content))] 20 | (if-not opts 21 | (.set goog.net.cookies k content) 22 | (.set goog.net.cookies k content (or max-age -1) path domain (boolean secure?))))) 23 | 24 | (defn- get-value [k] 25 | (.get goog.net.cookies k)) 26 | 27 | (defn get 28 | "gets the value at the key (as edn), optional default when value is not found" 29 | [k & [default]] 30 | (or (some-> k name get-value reader/read-string) default)) 31 | 32 | (defn contains-key? 33 | "is the key present in the cookies" 34 | [k] 35 | (.containsKey goog.net.cookies (name k))) 36 | 37 | (defn empty? 38 | "true if no cookies are set" 39 | [] 40 | (.isEmpty goog.net.cookies)) 41 | 42 | (defn remove! 43 | "removes a cookie, optionally for a specific path and/or domain" 44 | ([k] 45 | (.remove goog.net.cookies (name k))) 46 | ([k path domain] 47 | (.remove goog.net.cookies (name k) path domain))) 48 | -------------------------------------------------------------------------------- /src/crux_ui/events/default_db.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.events.default-db 2 | (:require [clojure.string :as s] 3 | [crux-ui.cookies :as c] 4 | [crux-ui.views.commons.dom :as dom])) 5 | 6 | (def example-query-str 7 | (s/join "\n" 8 | ["{:find [e]" 9 | " :where" 10 | " [[e :crux.db/id _]]" 11 | "; options" 12 | " :full-results? true}"])) 13 | 14 | (def ^:private now (js/Date.)) 15 | 16 | (def default-db 17 | {:db.query/input example-query-str 18 | :db.query/time {:time/vt nil :time/tt nil} 19 | :db.query/input-committed example-query-str 20 | :db.query/limit 10000 21 | :db.query.attr-history/docs-limit 10000 22 | :db.sys/host (dom/calc-initial-host) 23 | :db.sys/initialized? false 24 | :db.sys/route {:r/handler :rd/query-ui :r/query-params {:r/output-tab :db.ui.output-tab/table}} 25 | :db.ui/root-tab :db.ui.root-tab/query-ui 26 | :db.ui/output-side-tab nil ;:db.ui.output-tab/table 27 | :db.ui/editor-key 0 28 | :db.ui/show-form? true 29 | :db.ui/second-layer false 30 | :db.ui.second-layer/main-pane nil 31 | :db.ui.attr-history/hint? true 32 | :db.ui/display-mode :ui.display-mode/query 33 | :db.ui/screen-size {:ui.screen/inner-width js/window.innerWidth 34 | :ui.screen/inner-height js/window.innerHeight} 35 | :db.ui.examples/closed? (c/get :db.ui.examples/closed? false) 36 | :db.query/key 0 37 | :db.query/error nil 38 | :db.query/result nil}) 39 | -------------------------------------------------------------------------------- /src/crux_ui/events/effects.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.events.effects 2 | (:require [re-frame.core :as rf] 3 | [crux-ui.io.query :as q] 4 | [crux-ui.routes :as routes] 5 | [crux-ui-lib.http-functions :as hf] 6 | [promesa.core :as p] 7 | [crux-ui.cookies :as c] 8 | [crux-ui.logging :as log])) 9 | 10 | (rf/reg-fx 11 | :fx/query-exec 12 | (fn [{:keys [raw-input query-analysis] :as query}] 13 | (if query 14 | (q/exec query)))) 15 | 16 | (defn qs [_] 17 | (q/fetch-stats)) 18 | 19 | (rf/reg-fx 20 | :fx/query-stats 21 | qs) 22 | 23 | (rf/reg-fx 24 | :fx/set-node 25 | (fn [node-addr] 26 | (when node-addr 27 | (q/set-node! (str "//" node-addr)) 28 | (q/ping-status) 29 | (q/fetch-stats)))) 30 | 31 | (rf/reg-fx 32 | :fx.query/history 33 | (fn [history-query] 34 | (q/fetch-histories history-query))) 35 | 36 | (rf/reg-fx 37 | :fx.query/histories-docs 38 | (fn [eids->histories] 39 | (q/fetch-histories-docs eids->histories))) 40 | 41 | (rf/reg-fx 42 | :fx/push-query-into-url 43 | (fn [^js/String query] 44 | (prn ::push-query query) 45 | (if (and query (< (.-length query) 2000)) 46 | (try 47 | (routes/push-query query) 48 | (catch js/Error e 49 | (log/warn "failed to push query")))))) 50 | 51 | (rf/reg-fx 52 | :fx.sys/set-cookie 53 | (fn [[cookie-name value]] 54 | (c/set! cookie-name value {:max-age 86400}))) 55 | 56 | (rf/reg-fx 57 | :fx.ui/alert 58 | (fn [message] 59 | (js/alert message))) 60 | 61 | (rf/reg-fx 62 | :fx.ui/prompt 63 | (fn [[message event-id]] 64 | (rf/dispatch [event-id (js/prompt message)]))) 65 | 66 | (rf/reg-fx 67 | :fx.ui/toggle-fullscreen 68 | (fn [_] 69 | (if js/document.fullscreen 70 | (js/document.exitFullscreen) 71 | (^js .requestFullscreen js/document.documentElement)))) 72 | 73 | (rf/reg-fx 74 | :fx/set-polling 75 | (fn [poll-interval-in-seconds] 76 | (some-> js/window.__console_polling_id js/clearInterval) 77 | (when poll-interval-in-seconds 78 | (let [ms (* 1000 poll-interval-in-seconds) 79 | iid (js/setInterval #(rf/dispatch [:evt.ui.query/poll-tick]) ms)] 80 | (set! js/window.__console_polling_id iid))))) 81 | 82 | (defn grab-gh-gist [gh-link] 83 | (-> (hf/fetch gh-link) 84 | (p/catch #(rf/dispatch [:evt.io/gist-err %])) 85 | (p/then #(rf/dispatch [:evt.io/gist-success (:body %)])))) 86 | 87 | (rf/reg-fx 88 | :fx/get-github-gist 89 | grab-gh-gist) 90 | -------------------------------------------------------------------------------- /src/crux_ui/functions.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.functions 2 | (:require [cljs.core.async :as async] 3 | [clojure.string :as str] 4 | [cljs.pprint :as pprint]) 5 | (:import [goog.object] 6 | [goog.async Debouncer])) 7 | 8 | (def jsget goog.object/getValueByKeys) 9 | 10 | (defn pprint-str [edn] 11 | (with-out-str (pprint/pprint edn))) 12 | 13 | (defn merge-with-keep [a] 14 | (apply merge-with (fn [v1 v2] ((if (vector? v1) conj vector) v1 v2)) a)) 15 | 16 | (defn debounce [f interval] 17 | (let [dbnc (Debouncer. f interval)] 18 | ;; We use apply here to support functions of various arities 19 | (fn [& args] (.apply (.-fire dbnc) dbnc (to-array args))))) 20 | 21 | (defn lines [& lines] (str/join "\n" lines)) 22 | 23 | (defn index-by 24 | "indexes a finite collection" 25 | [key-fn coll] 26 | (loop [remaining-items (seq coll) 27 | index-map (transient {})] 28 | (if-let [x (first remaining-items)] 29 | (recur (next remaining-items) 30 | (assoc! index-map (key-fn x) x)) 31 | (persistent! index-map)))) 32 | 33 | 34 | (defn map-map-values-vec [f m] 35 | (into {} (for [[k vs] m] [k (mapv f vs)]))) 36 | 37 | (defn map-keys [f m] 38 | (into {} (for [[k v] m] [(f k) v]))) 39 | 40 | (defn map-values [f m] 41 | (into {} (for [[k v] m] [k (f v)]))) 42 | 43 | (defn prefix-keys [prefix map] 44 | (map-keys #(keyword (str (name prefix) "/" (name %))) map)) 45 | 46 | (defn ks-vs-to-map [ks vs] 47 | (merge-with-keep (map #(zipmap ks %) vs))) 48 | 49 | (defn positions 50 | [pred coll] 51 | (keep-indexed (fn [idx x] 52 | (when (pred x) 53 | idx)) 54 | coll)) 55 | 56 | (defn qsort [f sb] 57 | (apply juxt (map (fn [s] (fn [x] (nth x (first (positions #{s} f))))) sb))) 58 | 59 | 60 | (defn format-date [d] 61 | (.toISOString (new js/Date d))) 62 | 63 | (defn decode-html [html] 64 | (let [txt (.createElement js/document "textarea")] 65 | (set! (.-innerHTML txt) html) 66 | (.. txt -value))) 67 | 68 | (defn strreplace [s] 69 | (clojure.string/replace s #"(?:\\r\\n|\\r|\\n)" "
")) 70 | ;/(?:\r\n|\r|\n)/g 71 | 72 | (defn e0? [v] 73 | (= 'e (first v))) 74 | 75 | -------------------------------------------------------------------------------- /src/crux_ui/io/query.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.io.query 2 | (:require [re-frame.core :as rf] 3 | [medley.core :as m] 4 | [crux-ui-lib.async-http-client :as crux-api] 5 | [promesa.core :as p] 6 | [cljs.reader :as reader] 7 | [crux-ui.logging :as log])) 8 | 9 | 10 | (defn post-opts [body] 11 | #js {:method "POST" 12 | :body body 13 | :headers #js {:Content-Type "application/edn"}}) 14 | 15 | 16 | (def debug? ^boolean goog.DEBUG) 17 | 18 | (def ^:private node-client (atom nil)) 19 | 20 | (defn set-node! 21 | "node-address crux node " 22 | [^js/String node-address] 23 | (reset! node-client (crux-api/new-api-client node-address))) 24 | 25 | (defn- on-exec-success [resp] 26 | (rf/dispatch [:evt.io/query-success resp])) 27 | 28 | (defn- on-stats-success [resp] 29 | (rf/dispatch [:evt.io/stats-success resp])) 30 | 31 | (defn- on-status-success [resp] 32 | (rf/dispatch [:evt.sys/node-connect-success resp])) 33 | 34 | (defn- on-tx-success [resp] 35 | (rf/dispatch [:evt.io/tx-success resp])) 36 | 37 | (defn- on-histories-success [eid->history] 38 | (rf/dispatch [:evt.io/histories-fetch-success eid->history])) 39 | 40 | (defn- on-histories-docs-success [eid->history] 41 | (rf/dispatch [:evt.io/histories-with-docs-fetch-success eid->history])) 42 | 43 | (defn- on-error [query-type err] 44 | (rf/dispatch [:evt.io/query-error 45 | {:evt/query-type query-type 46 | :evt/error (.-data err)}])) 47 | 48 | (defn- on-error--query [err] 49 | (on-error :crux.ui.query-type/query err)) 50 | 51 | (defn- on-error--tx [err] 52 | (on-error :crux.ui.query-type/tx err)) 53 | 54 | 55 | (defn exec-q [query-edn vt tt] 56 | (-> @node-client 57 | (crux-api/db vt tt) 58 | (crux-api/q query-edn) 59 | (p/then on-exec-success) 60 | (p/catch on-error--query))) 61 | 62 | (defn exec-tx [query-text] 63 | (-> @node-client 64 | (crux-api/submitTx query-text) 65 | (p/then on-tx-success) 66 | (p/catch on-error--tx))) 67 | 68 | (defn exec [{:keys [query-vt query-tt raw-input query-analysis] :as query}] 69 | (let [qtype (:crux.ui/query-type query-analysis)] 70 | (if-not query-analysis 71 | (println "err") ; TODO feedback to UI, or rather, UI shouldn't let it get this far 72 | (case qtype 73 | :crux.ui.query-type/query (exec-q raw-input query-vt query-tt) 74 | :crux.ui.query-type/tx (exec-tx raw-input))))) 75 | 76 | 77 | (defn fetch-stats [] 78 | (-> @node-client 79 | (crux-api/attributeStats) 80 | (p/then on-stats-success))) 81 | 82 | (defn ping-status [] 83 | (-> @node-client 84 | (crux-api/status) 85 | (p/then on-status-success))) 86 | 87 | (defn fetch-history [eid time-bounds docs-limit] 88 | (let [{:time/keys [tt-upper vt-upper]} time-bounds] 89 | (-> @node-client 90 | (crux-api/historyRange eid nil nil vt-upper tt-upper) 91 | (p/then #(take docs-limit %))))) 92 | ; (fetch-history :ids/fashion-ticker-2) 93 | 94 | (defn fetch-histories 95 | "Fetches histories, without docs" 96 | [{:q/keys [entity-ids time-bounds documents-per-entity-limit] 97 | :as history-query}] 98 | (-> (p/all (map #(fetch-history % time-bounds documents-per-entity-limit) entity-ids)) 99 | (p/then #(zipmap entity-ids %)) 100 | (p/then on-histories-success))) 101 | ; (fetch-histories [:ids/fashion-ticker-2]) 102 | 103 | (defn fetch-docs [hashes] 104 | (crux-api/documents @node-client hashes)) 105 | 106 | (defn- merge-docs-into-histories [eids->histories hash->doc] 107 | (let [with-doc 108 | (fn [{ch :crux.db/content-hash :as history-entry}] 109 | (assoc history-entry :crux.query/doc (hash->doc ch))) 110 | merge-docs-into-history 111 | (fn [eid history] 112 | (map with-doc history))] 113 | (m/map-kv-vals merge-docs-into-history eids->histories))) 114 | 115 | #_(merge-docs-into-histories 116 | {:ids/fashion-ticker-2 [{:crux.db/content-hash "49556fb568926e9f4d1d053f65fe86969baed9ab"} 117 | {:crux.db/content-hash "41f5d2050100fa23c0084042bda7a59d8d41d07b"}]} 118 | {"49556fb568926e9f4d1d053f65fe86969baed9ab" {:crux.db/id :ids/fashion-ticker-2, 119 | :ticker/price 77, 120 | :ticker/currency :currency/rub}, 121 | "41f5d2050100fa23c0084042bda7a59d8d41d07b" {:crux.db/id :ids/fashion-ticker-2, 122 | :ticker/price 75, 123 | :ticker/currency :currency/rub}}) 124 | 125 | 126 | (defn on-histories-docs-error [err] 127 | (println :on-histories-docs-error err)) 128 | 129 | (defn fetch-histories-docs 130 | "Fetches histories, without docs" 131 | [eids->histories] 132 | (let [doc-hashes (->> eids->histories vals flatten (map :crux.db/content-hash))] 133 | (-> (fetch-docs (set doc-hashes)) 134 | (p/then (fn [hash->doc] (merge-docs-into-histories eids->histories hash->doc))) 135 | (p/then on-histories-docs-success) 136 | (p/catch on-histories-docs-error)))) 137 | #_(fetch-histories-docs 138 | {:ids/fashion-ticker-2 139 | [{:crux.db/content-hash "49556fb568926e9f4d1d053f65fe86969baed9ab"} 140 | {:crux.db/content-hash "41f5d2050100fa23c0084042bda7a59d8d41d07b"}]}) 141 | 142 | (comment 143 | 144 | terr 145 | 146 | (-> '{:find [e] 147 | :args [{}] 148 | :where [[e :crux.db/id :ids/tech-ticker-2]]} 149 | pr-str 150 | exec-q)) 151 | -------------------------------------------------------------------------------- /src/crux_ui/logging.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.logging) 2 | 3 | (defn debug [& values] 4 | (apply (.-debug js/console) (map clj->js values))) 5 | 6 | (defn log [& values] 7 | (apply (.-log js/console) (map clj->js values))) 8 | 9 | (defn info [& values] 10 | (apply (.-info js/console) (map clj->js values))) 11 | 12 | (defn warn [& values] 13 | (apply (.-warn js/console) (map clj->js values))) 14 | 15 | (defn error [& values] 16 | (apply (.-error js/console) (map clj->js values))) -------------------------------------------------------------------------------- /src/crux_ui/logic/history_perversions.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.logic.history-perversions 2 | (:require [medley.core :as m])) 3 | 4 | (defn- simplify-history-entry 5 | [{:keys [crux.query/doc 6 | crux.db/content-hash 7 | crux.tx/tx-time 8 | crux.tx/tx-id 9 | crux.db/valid-time] 10 | :as entry}] 11 | (assoc doc :crux.db/valid-time valid-time 12 | :crux.tx/tx-time tx-time 13 | :crux.tx/tx-id tx-id 14 | :crux.db/content-hash content-hash)) 15 | 16 | (defn- simplify-entity-history [eid history] 17 | (map simplify-history-entry history)) 18 | 19 | (defn calc-entity-time-series [numeric-attrs eid->history-range] 20 | (m/map-kv-vals simplify-entity-history eid->history-range)) 21 | -------------------------------------------------------------------------------- /src/crux_ui/logic/plotting_data_calc.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.logic.plotting-data-calc) 2 | 3 | ; See https://plot.ly/javascript/line-and-scatter/ for docs 4 | 5 | (defn auto-name [v] 6 | (cond-> v (keyword? v) name)) 7 | 8 | (defn- attr-info-str 9 | [attr-key 10 | {:keys 11 | [crux.db/valid-time 12 | crux.db/content-hash 13 | crux.tx/tx-time 14 | crux.tx/tx-id] 15 | :as simple-history-entry}] 16 | (str (auto-name attr-key) ": " (get simple-history-entry attr-key) "
" 17 | "vt: " valid-time "
" 18 | "tt: " tx-time "
" 19 | "tx-id: " tx-id "
" 20 | "hash: " content-hash)) 21 | 22 | (defn calc-plotly-trace--attr [attr-key eid simple-history] 23 | {:name (auto-name eid) 24 | :type "scatter" 25 | :text (map (partial attr-info-str attr-key) simple-history) 26 | :x (map :crux.db/valid-time simple-history) 27 | :y (map attr-key simple-history)}) 28 | 29 | 30 | (defn- tx-info-str 31 | [{:keys [crux.db/valid-time crux.tx/tx-time crux.tx/tx-id]}] 32 | (str "vt: " valid-time "
" 33 | "tt: " tx-time "
" 34 | "tx-id" tx-id)) 35 | 36 | (defn calc-plotly-trace--tx-scatter [eid txes] 37 | {:name (auto-name eid) 38 | :mode "markers" 39 | :type "scatter" 40 | :text (map tx-info-str txes) 41 | :x (map :crux.db/valid-time txes) 42 | :y (map :crux.tx/tx-time txes)}) 43 | -------------------------------------------------------------------------------- /src/crux_ui/logic/query_analysis.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.logic.query-analysis 2 | (:require [medley.core :as m] 3 | [clojure.set :as cset] 4 | [cljs.reader :as reader] 5 | [crux-ui-lib.functions :as fns])) 6 | 7 | 8 | (defn calc-vector-headers [query-vector] 9 | (->> query-vector 10 | (drop-while #(not= :find %)) 11 | (rest) 12 | (take-while #(not= (keyword? %))))) 13 | 14 | 15 | (defn analyse-full-results-headers [query-results-seq] 16 | (let [res-count (count query-results-seq) 17 | sample (if (> res-count 50) 18 | (random-sample (/ 50 res-count) query-results-seq) 19 | query-results-seq)] 20 | (set (flatten (map (comp keys first) sample))))) 21 | 22 | (defn single-tx-vec->map [[type doc-id doc vt tt]] 23 | {:crux.ui/query-type :crux.ui.query-type/tx 24 | :crux.tx/type type 25 | :crux.db/id doc-id 26 | :crux.db/doc doc 27 | :crux.ui/vt vt 28 | :crux.ui/tt tt}) 29 | 30 | (defn multi-tx-vec->map [txes-vector] 31 | (let [tx-infos (map single-tx-vec->map txes-vector)] 32 | {:crux.ui/query-type :crux.ui.query-type/tx 33 | :tx-count (count tx-infos) 34 | :tx-infos tx-infos})) 35 | 36 | (defn try-parse-edn-string-or-nil [^js/String str] 37 | (try 38 | (reader/read-string str) 39 | (catch js/Error e 40 | nil))) 41 | 42 | (defn try-parse-edn-string [^js/String str] 43 | (try 44 | (reader/read-string str) 45 | (catch js/Error e 46 | {:error e}))) 47 | 48 | (defn query-vector? [edn] 49 | (and (vector? edn) (= :find (first edn)))) 50 | 51 | (def crux-tx-types-set 52 | #{:crux.tx/put :crux.tx/cas :crux.tx/delete :crux.tx/evict}) 53 | 54 | (defn single-tx-vector? [edn] 55 | (and (vector? edn) (crux-tx-types-set (first edn)))) 56 | 57 | (defn multi-tx-vector? [edn] 58 | (and (vector? edn) (not-empty edn) (every? single-tx-vector? edn))) 59 | 60 | (defn query-map? [edn] 61 | (and (map? edn) (every? edn [:find :where]))) 62 | 63 | (defn third [coll] 64 | (nth coll 2 nil)) 65 | 66 | (defn infer-symbol-attr-map 67 | "Given a simple datalog query map returns a map symbol -> attribute" 68 | [qmap] 69 | (let [symbols-queried (set (:find qmap)) 70 | where-vec (:where qmap) 71 | attr-triplets (filter (comp keyword? second) where-vec) 72 | eid-triplets (filter (comp symbol? first) where-vec) 73 | retained-triplets (filter #(symbols-queried (third %)) attr-triplets) 74 | eid-symbols (set (map first eid-triplets)) 75 | retained-eids (set (filter symbols-queried eid-symbols)) 76 | lookup-symbol-attr (fn [symbol] 77 | (second (m/find-first #(= (third %) symbol) retained-triplets))) 78 | symbol-attr-pair (fn [symbol] 79 | [symbol 80 | (if (retained-eids symbol) 81 | :crux.db/id 82 | (lookup-symbol-attr symbol))]) 83 | symbol->attr (into {} (map symbol-attr-pair symbols-queried))] 84 | symbol->attr)) 85 | 86 | 87 | (defn analyse-query [input-edn] 88 | (let [qmap (fns/normalize-query input-edn) 89 | s->a (infer-symbol-attr-map qmap) 90 | a->s (cset/map-invert s->a) 91 | attr-seq (vals s->a) 92 | attr-set (set attr-seq) 93 | find-vec (:find qmap) 94 | symbol-positions (into {} (map vector find-vec (range))) 95 | attr-positions (zipmap attr-seq (map (comp symbol-positions a->s) attr-seq))] 96 | (assoc qmap :crux.ui/query-type :crux.ui.query-type/query 97 | :query/style (if (map? input-edn) :query.style/map :query.style/vector) 98 | :query/ids-queried? (attr-set :crux.db/id) 99 | :query/symbol-positions symbol-positions 100 | :query/original-edn input-edn 101 | :query/normalized-edn qmap 102 | :query/attr-set attr-set 103 | :query/attr-vec (mapv s->a find-vec) 104 | :query/attr-positions attr-positions 105 | :query/pos->attr (cset/map-invert attr-positions) 106 | :query/attributes s->a))) 107 | 108 | (defn analyse-any-query [input-edn] 109 | (try 110 | (cond 111 | (query-vector? input-edn) (analyse-query input-edn) 112 | (multi-tx-vector? input-edn) (multi-tx-vec->map input-edn) 113 | (query-map? input-edn) (analyse-query input-edn) 114 | :else false) 115 | (catch js/Error e 116 | false))) 117 | 118 | (defn- calc-numeric-keys [result-map] 119 | (map first (filter (comp number? second) result-map))) 120 | 121 | (defn analyse-results 122 | [{:query/keys 123 | [attr-set 124 | attr-vec] 125 | :as query-info} 126 | results] 127 | (if (and (= (:crux.ui/query-type query-info) :crux.ui.query-type/query) 128 | (not-empty results)) 129 | (let [r-count (count results) 130 | ids-received? (or (attr-set :crux.db/id) (:full-results? query-info)) 131 | full-results? (:full-results? query-info) 132 | first-res (if full-results? 133 | (-> results first first) 134 | (-> results first)) 135 | first-res-map (if full-results? 136 | first-res 137 | (zipmap attr-vec first-res)) 138 | ids-pluck 139 | (cond 140 | full-results? (comp :crux.db/id first) 141 | ids-received? first 142 | :else identity) 143 | numeric-attrs (disj (set (calc-numeric-keys first-res-map)) :crux.db/id) 144 | discrete-attrs (cset/difference (disj attr-set :crux.db/id) numeric-attrs) 145 | ids (if ids-received? (map ids-pluck results))] 146 | {:ra/single-entity? (= 1 r-count) 147 | :ra/results-count r-count 148 | :ra/has-results? (> r-count 0) 149 | :ra/numeric-attrs numeric-attrs 150 | :ra/has-numeric-attrs? (> (count numeric-attrs) 0) 151 | :ra/discrete-attrs discrete-attrs 152 | :ra/entity-ids ids 153 | :ra/entity-id (first ids) 154 | :ra/first-res-map first-res-map 155 | :ra/first-res first-res}) 156 | (println :bailing-out query-info results))) 157 | -------------------------------------------------------------------------------- /src/crux_ui/logic/readme.md: -------------------------------------------------------------------------------- 1 | This package is for pure functions of different domains -------------------------------------------------------------------------------- /src/crux_ui/logic/time.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.logic.time 2 | (:require [goog.string :as gs])) 3 | 4 | (defn format-iso-8601--utc [^js/Date d] 5 | (str "" (.getUTCFullYear d) 6 | "-" (gs/padNumber (inc (.getUTCMonth d)) 2) 7 | "-" (gs/padNumber (.getUTCDate d) 2) 8 | "T" (gs/padNumber (.getUTCHours d) 2) 9 | ":" (gs/padNumber (.getUTCMinutes d) 2) 10 | ":" (gs/padNumber (.getUTCSeconds d) 2) 11 | "." (gs/padNumber (.getUTCMilliseconds d) 3) 12 | "Z")) 13 | 14 | (defn format-for-dt-local [^js/Date d] 15 | (str "" (.getFullYear d) 16 | "-" (gs/padNumber (inc (.getMonth d)) 2) 17 | "-" (gs/padNumber (.getDate d) 2) 18 | "T" (gs/padNumber (.getHours d) 2) 19 | ":" (gs/padNumber (.getMinutes d) 2))) 20 | 21 | (def user-time-zone 22 | (.. js/Intl (^js DateTimeFormat) (^js resolvedOptions) ^js -timeZone)) 23 | 24 | 25 | (defn date->comps [^js/Date t] 26 | {:time/year (.getFullYear t) 27 | :time/month (inc (.getMonth t)) 28 | :time/date (.getDate t) 29 | :time/hour (.getHours t) 30 | :time/minute (.getMinutes t)}) 31 | 32 | (defn comps->date [comps] 33 | (let [date-str (str (:time/year comps) "-" 34 | (gs/padNumber (:time/month comps) 2) "-" 35 | (gs/padNumber (:time/date comps) 2) " " 36 | (gs/padNumber (:time/hour comps) 2) ":" 37 | (gs/padNumber (:time/minute comps) 2)) 38 | ts (js/Date.parse date-str)] 39 | (if-not (js/isNaN ts) 40 | (js/Date. ts)))) 41 | 42 | (comment 43 | (let [d (js/Date.)] 44 | (date->comps d) 45 | (comps->date (date->comps d)))) 46 | -------------------------------------------------------------------------------- /src/crux_ui/main.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.main 2 | (:require [reagent.core :as r] 3 | [crux-ui.subs] 4 | [re-frame.core :as rf] 5 | [crux-ui.views.facade :as views] 6 | [crux-ui.events.facade] 7 | [crux-ui.routes :as routes] 8 | [crux-ui.events.default-db :as d] 9 | [clojure.string :as s] 10 | [crux-ui.logging :as log])) 11 | 12 | 13 | 14 | (set! js/window.tojs clj->js) 15 | 16 | (defn mount-root [] 17 | (r/render [views/root] (js/document.getElementById "app"))) 18 | 19 | (defn dispatch-screen-measurements [] 20 | (rf/dispatch 21 | [:evt.ui/screen-resize 22 | {:ui.screen/inner-height js/window.innerHeight 23 | :ui.screen/inner-width js/window.innerWidth}])) 24 | 25 | (defn- listen-keyboard-shortcuts [evt] 26 | (when (and (.-ctrlKey evt) (= 69 (.-keyCode evt))) 27 | (rf/dispatch [:evt.keyboard/ctrl-e])) 28 | (when (and (.-ctrlKey evt) (= 13 (.-keyCode evt))) 29 | (rf/dispatch [:evt.keyboard/ctrl-enter]))) 30 | 31 | (defn lookup-gist [] 32 | (let [search js/location.search 33 | re-expls #"\?examples-gist="] 34 | (when (re-find re-expls search) 35 | (println :examples-gist-link-found (s/replace search re-expls "")) 36 | (rf/dispatch [:evt.ui/github-examples-request (s/replace search re-expls "")])))) 37 | 38 | ; (lookup-gist) 39 | 40 | (defn init [] 41 | (js/window.addEventListener "keydown" listen-keyboard-shortcuts true) 42 | (js/window.addEventListener "resize" dispatch-screen-measurements) 43 | (rf/dispatch-sync [:evt.db/init d/default-db]) 44 | (routes/init) 45 | (lookup-gist) 46 | (js/setTimeout dispatch-screen-measurements 50) 47 | (mount-root)) 48 | 49 | ;; This is called every time you make a code change 50 | (defn reload! [] 51 | (mount-root)) 52 | -------------------------------------------------------------------------------- /src/crux_ui/main_perf.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.main-perf 2 | (:require [reagent.core :as r] 3 | [crux-ui.subs] 4 | [re-frame.core :as rf] 5 | [crux-ui.views.facade-perf :as views] 6 | [crux-ui.events.facade :as events] 7 | [clojure.string :as s] 8 | [crux-ui.events.default-db :as d] 9 | [crux-ui.cookies :as c])) 10 | 11 | 12 | (set! js/window.tojs clj->js) 13 | 14 | (defn mount-root [] 15 | (r/render [views/root] (js/document.getElementById "app"))) 16 | 17 | (defn- listen-keyboard-shortcuts [evt] 18 | (when (and (.-ctrlKey evt) (= 13 (.-keyCode evt))) 19 | (rf/dispatch [:evt.keyboard/ctrl-enter]))) 20 | 21 | (defn init [] 22 | (js/window.addEventListener "keydown" listen-keyboard-shortcuts) 23 | (rf/dispatch-sync [:evt.db/init d/default-db]) 24 | (mount-root)) 25 | 26 | ;; This is called every time you make a code change 27 | (defn reload! [] 28 | (mount-root)) 29 | -------------------------------------------------------------------------------- /src/crux_ui/routes.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.routes 2 | (:require [bidi.bidi :as bidi] 3 | [re-frame.core :as rf] 4 | [crux-ui.logging :as log] 5 | [crux-ui.views.commons.dom :as dom] 6 | [crux-ui.functions :as f] 7 | [medley.core :as m])) 8 | 9 | (def prefix 10 | (str (or (dom/get-routes-prefix) "/console"))) 11 | 12 | (def app-routes-base (str prefix "/app")) 13 | 14 | (def ^:private routes 15 | [app-routes-base 16 | {"" :rd/query-ui 17 | 18 | "/output" 19 | {"" :rd/query-ui 20 | ["/" :r/output-tab] :rd/query-ui-output-tab} 21 | 22 | "/settings" 23 | {"" :rd/settings 24 | ["/" :r/tab] :rd/settings} 25 | 26 | ["/example/" :rd/example-id] :rd/query-ui}]) 27 | 28 | (defn- prefix-keys [route] 29 | (f/map-keys #(keyword "r" (name %)) route)) 30 | 31 | (defn- match-route [path] 32 | (if-let [route (prefix-keys (bidi/match-route routes path))] 33 | (cond-> route 34 | (get-in route [:r/route-params :r/output-tab]) 35 | (update-in [:r/route-params :r/output-tab] 36 | #(keyword "db.ui.output-tab" %))))) 37 | 38 | (def ^:private path-for (partial bidi/path-for routes)) 39 | 40 | (defn path-for-tab [tab-name {:r/keys [search]}] 41 | (str (path-for :rd/query-ui-output-tab 42 | :r/output-tab 43 | (name tab-name)) 44 | search)) 45 | 46 | (defn query-str->map [^js/String query] 47 | (let [raw-map (into {} (js->clj (js/Array.from (js/URLSearchParams. query))))] 48 | (f/map-keys #(if (string? %) (keyword "rd" %) %) raw-map))) 49 | 50 | (defn ^js/String query-map->str [m] 51 | (.toString (js/URLSearchParams. (clj->js m)))) 52 | 53 | (defn- url-for [route-vector query-map] 54 | (let [path (apply path-for route-vector) 55 | query-str (query-map->str query-map)] 56 | (str path "?" query-str))) 57 | 58 | 59 | (defn- calc-route-data-from-location [] 60 | (let [route-data (match-route js/location.pathname) 61 | query-map (query-str->map js/location.search)] 62 | (assoc route-data :r/query-params query-map))) 63 | 64 | (defn- on-pop-state [evt] 65 | (rf/dispatch [:evt.sys/set-route (calc-route-data-from-location)])) 66 | 67 | (defn- calc-query-url [route-data crux-query-str] 68 | (let [handler-id (route-data :r/handler) 69 | route-params (-> (:r/route-params route-data) 70 | (m/update-existing :r/output-tab name)) 71 | route-params (vec (flatten (into [handler-id] route-params)))] 72 | (prn ::route-params route-params) 73 | (url-for route-params {:rd/query crux-query-str}))) 74 | 75 | (comment 76 | (calc-query-url (calc-route-data-from-location) "{:find [e]}")) 77 | 78 | (defn- push-query [crux-query-str] 79 | (js/history.pushState nil "Crux Console : user query" (calc-query-url (calc-route-data-from-location) crux-query-str))) 80 | 81 | (defn- on-link-clicks [click-evt] 82 | (let [target (dom/jsget click-evt "target") 83 | tagname (dom/jsget target "tagName") 84 | href (dom/jsget target "href") 85 | is-anchor? (= "A" tagname) 86 | url (if is-anchor? 87 | (try 88 | (js/URL. href) 89 | (catch js/Error e 90 | (log/error "can't parse link url, will ignore it" href) 91 | nil))) 92 | pathname (and url (dom/jsget url "pathname")) 93 | route-data (and pathname (match-route pathname))] 94 | (when (and url is-anchor? route-data) 95 | (.preventDefault click-evt) 96 | (js/history.pushState nil "Crux Console" href) 97 | (rf/dispatch [:evt.sys/set-route route-data])))) 98 | 99 | (defn init [] 100 | (js/window.addEventListener "popstate" on-pop-state false) 101 | (js/window.addEventListener "click" on-link-clicks true) 102 | (let [route-data (calc-route-data-from-location)] 103 | (rf/dispatch [:evt.sys/set-route route-data]))) 104 | -------------------------------------------------------------------------------- /src/crux_ui/svg_icons.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.svg-icons) 2 | 3 | (defn- icon [icon-name svg] 4 | [:div.svg-icon {:class (str "svg-icon--" (name icon-name))} 5 | svg]) 6 | 7 | (def external 8 | (icon :external 9 | [:svg#Capa_1 {:xmlns "http://www.w3.org/2000/svg" :version "1.1" :x "0px" :y "0px" :viewBox "0 0 511.626 511.627" 10 | :style {:enable-background "new 0 0 511.626 511.627"} :xml:space "preserve"} 11 | [:path {:d "M392.857,292.354h-18.274c-2.669,0-4.859,0.855-6.563,2.573c-1.718,1.708-2.573,3.897-2.573,6.563v91.361 c0,12.563-4.47,23.315-13.415,32.262c-8.945,8.945-19.701,13.414-32.264,13.414H82.224c-12.562,0-23.317-4.469-32.264-13.414 c-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562,4.471-23.313,13.417-32.259c8.947-8.947,19.702-13.418,32.264-13.418 h200.994c2.669,0,4.859-0.859,6.57-2.57c1.711-1.713,2.566-3.9,2.566-6.567V82.221c0-2.662-0.855-4.853-2.566-6.563 c-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648,0-42.016,8.042-58.102,24.125C8.042,113.297,0,132.665,0,155.313v237.542 c0,22.647,8.042,42.018,24.123,58.095c16.086,16.084,35.454,24.13,58.102,24.13h237.543c22.647,0,42.017-8.046,58.101-24.13 c16.085-16.077,24.127-35.447,24.127-58.095v-91.358c0-2.669-0.856-4.859-2.574-6.57 C397.709,293.209,395.519,292.354,392.857,292.354z"}] 12 | [:path {:d "M506.199,41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948,0-9.233,1.807-12.847,5.424 c-3.617,3.615-5.428,7.898-5.428,12.847s1.811,9.233,5.428,12.85l50.247,50.248L198.424,304.067 c-1.906,1.903-2.856,4.093-2.856,6.563c0,2.479,0.953,4.668,2.856,6.571l32.548,32.544c1.903,1.903,4.093,2.852,6.567,2.852 s4.665-0.948,6.567-2.852l186.148-186.148l50.251,50.248c3.614,3.617,7.898,5.426,12.847,5.426s9.233-1.809,12.851-5.426 c3.617-3.616,5.424-7.898,5.424-12.847V54.818C511.626,49.866,509.813,45.586,506.199,41.971z"}]])) 13 | 14 | (def close 15 | (icon :close 16 | [:svg.svg-icon__content {:viewBox "0 0 1000 1000" :xmlns "http://www.w3.org/2000/svg"} 17 | [:path {:d "M1008 898.915l-45.085 45.085-450.915-450.915-450.915 450.915-45.085-45.085 450.915-450.915-450.915-450.915 45.085-45.085 450.915 450.915 450.915-450.915 45.085 45.085-450.915 450.915z"}]])) 18 | 19 | 20 | (def styles 21 | [[:.svg-icon 22 | {:width :12px 23 | :height :12px 24 | :color :grey} 25 | [:&__content 26 | {:width :100% 27 | :height :100%}]]]) 28 | -------------------------------------------------------------------------------- /src/crux_ui/ua-regexp.js: -------------------------------------------------------------------------------- 1 | module.exports = /(MSIE|Trident|(?!Gecko.+)Firefox|(?!AppleWebKit.+Chrome.+)Safari(?!.+Edge)|(?!AppleWebKit.+)Chrome(?!.+Edge)|(?!AppleWebKit.+Chrome.+Safari.+)Edge|AppleWebKit(?!.+Chrome|.+Safari)|Gecko(?!.+Firefox))(?: |\/)([\d\.apre]+)/ -------------------------------------------------------------------------------- /src/crux_ui/views/attr_stats.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.attr-stats 2 | (:require [garden.core :as garden] 3 | [re-frame.core :as rf] 4 | [crux-ui.views.output.table :as q-results-table] 5 | [crux-ui.views.style :as s])) 6 | 7 | (def ^:private -sub-attr-stats 8 | (rf/subscribe [:subs.query/attr-stats-table])) 9 | 10 | (def ^:private style 11 | [:style 12 | (garden/css 13 | [:.attr-stats 14 | {:height :100%} 15 | [:&__footer 16 | {:text-align :center 17 | :padding :16px}]])]) 18 | 19 | (defn root [] 20 | [:div.attr-stats 21 | style 22 | [q-results-table/root @-sub-attr-stats] 23 | [:footer.attr-stats__footer 24 | [:strong "Indexed attributes frequencies"]]]) 25 | -------------------------------------------------------------------------------- /src/crux_ui/views/charts/custom-plotly--console.js: -------------------------------------------------------------------------------- 1 | // in custom-plotly--perf.js 2 | var Plotly = require('plotly.js/lib/core'); 3 | 4 | // Load in the trace types for pie, and choropleth 5 | Plotly.register([ 6 | require('plotly.js/lib/scatter') 7 | ]); 8 | 9 | module.exports = Plotly; -------------------------------------------------------------------------------- /src/crux_ui/views/charts/custom-plotly--perf.js: -------------------------------------------------------------------------------- 1 | // in custom-plotly--perf.js 2 | require('graceful-fs/graceful-fs'); 3 | require('gl-surface3d/lib/shaders'); 4 | require('gl-surface3d/surface'); 5 | require('gl-surface3d'); 6 | var Plotly = require('plotly.js/lib/core'); 7 | 8 | // Load in the trace types for pie, and choropleth 9 | Plotly.register([ 10 | require('plotly.js/lib/surface') 11 | ]); 12 | 13 | module.exports = Plotly; -------------------------------------------------------------------------------- /src/crux_ui/views/charts/query_perf_plots.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.charts.query-perf-plots 2 | (:require ["plotly.js-gl3d-dist" :as Plotly] 3 | [reagent.core :as r] 4 | [garden.core :as css] 5 | [garden.core :as garden] 6 | [cljs.tools.reader.edn :as edn] 7 | [crux-ui.logging :as log])) 8 | 9 | 10 | (def ^:private plot-styling 11 | [:style 12 | (garden/css 13 | [:.plotly-container 14 | {:height :100%}])]) 15 | 16 | (def colors 17 | #{"YIGnBu" "Portland" "Picnic"}) 18 | 19 | (defn z-data [{:keys [plain with-cache] :as query-data}] 20 | (clj->js 21 | [{:z (:data plain) 22 | :name "Cache off" 23 | :colorscale "Picnic" 24 | :type "surface"} 25 | {:z (:data with-cache) 26 | :name "Cache on" 27 | :colorscale "Viridis" 28 | :type "surface"}])) 29 | 30 | 31 | (defn axis [{:keys [title ticks] :as axis}] 32 | {:title (name (:title axis)) 33 | :nticks (count ticks)}) 34 | 35 | (defn opts [{:keys [title with-cache plain] :as query-data}] 36 | (clj->js 37 | {:title title 38 | :autosize true 39 | :showlegend true 40 | :height 900 41 | :width 1200 42 | :scene 43 | {:xaxis (axis (:x with-cache)) ; x is history days 44 | :yaxis (axis (:y with-cache)) ; y is stocks count 45 | :zaxis {:title "ms"}} 46 | :margin 47 | {:l 65, 48 | :r 50, 49 | :b 65, 50 | :t 90}})) 51 | 52 | (defn do-plot [container query-data] 53 | (.newPlot Plotly container (z-data query-data) (opts query-data))) 54 | 55 | (defn root 56 | [data] 57 | (let [-inst (atom nil)] 58 | (r/create-class 59 | {:component-did-mount 60 | (fn [this] 61 | (let [el (r/dom-node this) 62 | inst (do-plot el data)] 63 | (reset! -inst inst))) 64 | 65 | :reagent-render 66 | (fn [_ _ _] 67 | [:div.plotly-container 68 | [:style plot-styling]])}))) 69 | -------------------------------------------------------------------------------- /src/crux_ui/views/charts/wrapper_3d.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.charts.wrapper-3d 2 | "For responsiveness ensure that parent element is responsive and takes exactly 100% 3 | of the desired viewport." 4 | (:require [reagent.core :as r] 5 | ["plotly.js-gl3d-dist" :as Plotly] 6 | [garden.core :as garden])) 7 | 8 | 9 | (def ^:private plot-styling 10 | [:style 11 | (garden/css 12 | [:.plotly-container 13 | {:height :100%}])]) 14 | 15 | (defn plotly-wrapper 16 | [data layout] 17 | (let [-inst (atom nil)] 18 | (r/create-class 19 | {:component-did-mount 20 | (fn [this] 21 | (reset! -inst (.newPlot Plotly 22 | (r/dom-node this) 23 | (clj->js data) 24 | (clj->js layout) 25 | #js {:responsive true}))) 26 | 27 | :reagent-render 28 | (fn [_ _ _] 29 | [:div.plotly-container 30 | [:style plot-styling]])}))) -------------------------------------------------------------------------------- /src/crux_ui/views/charts/wrapper_basic.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.charts.wrapper-basic 2 | "For responsiveness ensure that parent element is responsive and takes exactly 100% 3 | of the desired viewport." 4 | (:require [reagent.core :as r] 5 | ; ["plotly.js-basic-dist" :as Plotly] 6 | ["./custom-plotly--console" :as Plotly] 7 | [garden.core :as garden])) 8 | 9 | 10 | (def ^:private plot-styling 11 | [:style 12 | (garden/css 13 | [:.plotly-container 14 | {:height :100%}])]) 15 | 16 | (defn plotly-wrapper 17 | [data layout] 18 | (let [-inst (atom nil)] 19 | (r/create-class 20 | {:component-did-mount 21 | (fn [this] 22 | (reset! -inst (.newPlot Plotly 23 | (r/dom-node this) 24 | (clj->js data) 25 | (clj->js layout) 26 | #js {:responsive true}))) 27 | 28 | :reagent-render 29 | (fn [_ _ _] 30 | [:div.plotly-container 31 | [:style plot-styling]])}))) -------------------------------------------------------------------------------- /src/crux_ui/views/cluster_health.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.cluster-health 2 | (:require [cljs.pprint])) 3 | 4 | 5 | 6 | (def cluster-mock 7 | (let [protonode {:crux.version/version "19.04-1.0.1-alpha", 8 | :crux.kv/size 792875, 9 | :crux.index/index-version 4, 10 | :crux.tx-log/consumer-state 11 | {:crux.tx/event-log 12 | {:lag 0, :next-offset 1592957991111682, 13 | :time #inst "2019-04-18T21:30:38.195-00:00"}}} 14 | protonode2 {:crux.version/version "19.04-1.0.0-alpha", 15 | :crux.kv/size 792875, 16 | :crux.index/index-version 4, 17 | :crux.tx-log/consumer-state 18 | {:crux.tx/event-log 19 | {:lag 0, :next-offset 1592957988161759, 20 | :time #inst "2019-04-18T21:15:12.561-00:00"}}}] 21 | [["http://node-1.crux.cloud:8080" [:span "Status: OK " [:span {:style {:font-weight "bold" :color "green"}} "✓"]] protonode] 22 | ["http://node-2.crux.cloud:8080" [:span "Status: OK " [:span {:style {:font-weight "bold" :color "green"}} "✓"]] protonode] 23 | ["http://node-3.crux.cloud:8080" [:span "Error: " [:span {:style {:font-weight "normal" :color "red"}} "Connection Timeout ✘"]] protonode2]])) 24 | 25 | (defn root [] 26 | [:div.cluster-health 27 | [:h2 "Cluster Health"] 28 | [:div.cg {:style {:display "grid" 29 | :margin-right "auto" 30 | :margin-left "auto" 31 | :grid-template-columns "2em auto auto auto" 32 | :grid-row-gap "1em"}} 33 | 34 | (mapcat identity 35 | (for [n cluster-mock] 36 | [ 37 | [:div {:key (str n 0)} [:a {:style {:text-decoration "none" :font-weight "bold" :font-size "1.3em"}} "↻"]] 38 | [:div {:key (str n 1)}[:a (nth n 0)]] 39 | [:div {:key (str n 2)}(nth n 1)] 40 | [:div {:key (str n 3)}[:pre (with-out-str (cljs.pprint/pprint (nth n 2)))]]])) 41 | 42 | 43 | 44 | [:div {:style {:height "1em"}}] 45 | [:a "Refresh All"]]]) 46 | -------------------------------------------------------------------------------- /src/crux_ui/views/codemirror.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.codemirror 2 | (:require [reagent.core :as r] 3 | [garden.core :as garden] 4 | [goog.object :as gobj] 5 | ["/codemirror/lib/codemirror.js" :as codemirror] 6 | ["/codemirror/mode/clojure/clojure.js"] 7 | ["/codemirror/addon/edit/closebrackets.js"] 8 | ["/codemirror/addon/edit/matchbrackets.js"] 9 | ["/codemirror/addon/hint/show-hint"] 10 | ["/codemirror/addon/hint/anyword-hint"])) 11 | 12 | 13 | (def code-mirror-styling 14 | (garden/css 15 | [[:.code-mirror-container 16 | {:font-size :17px 17 | :padding "0px 0px" 18 | :overflow :hidden 19 | :height :100%}] 20 | [:.CodeMirror 21 | {:border-radius :2px 22 | :height :100%}]])) 23 | 24 | (defn escape-re [input] 25 | (let [re (js/RegExp. "([.*+?^=!:${}()|[\\]\\/\\\\])" "g")] 26 | (-> input str (.replace re "\\$1")))) 27 | 28 | (defn fuzzy-re [input] 29 | (-> (reduce (fn [s c] (str s (escape-re c) ".*")) "" input) 30 | (js/RegExp "i"))) 31 | 32 | (def ^{:private true :const true} crux-builtin-keywords 33 | [:find :where :args :rules :offset :limit :order-by 34 | :timeout :full-results? :not :not-join :or :or-join 35 | :range :unify :rule :pred :ui/poll-interval-seconds?]) 36 | 37 | 38 | (defn- autocomplete [index cm options] 39 | (let [cur (.getCursor cm) 40 | line (.-line cur) 41 | ch (.-ch cur) 42 | token (.getTokenAt cm cur) 43 | reg (subs (.-string token) 0 (- ch (.-start token))) 44 | blank? (#{"[" "{" " " "("} reg) 45 | start (if blank? cur (.Pos codemirror line (gobj/get token "start"))) 46 | end (if blank? cur (.Pos codemirror line (gobj/get token "end"))) 47 | words (concat crux-builtin-keywords index) 48 | fuzzy (if blank? #".*" (fuzzy-re reg)) 49 | words (->> words 50 | (map str) 51 | (filter #(re-find fuzzy %)))] 52 | (clj->js {:list words 53 | :from start 54 | :to end}))) 55 | 56 | 57 | (defn code-mirror 58 | [initial-value {:keys [read-only? stats on-change on-cm-init]}] 59 | 60 | (let [value-atom (atom (or initial-value "")) 61 | on-change (or on-change (constantly nil)) 62 | cm-inst (atom nil) 63 | indexes (when (map? stats) (keys stats))] 64 | (r/create-class 65 | 66 | {:component-did-mount 67 | (fn [this] 68 | (let [el (r/dom-node this) 69 | opts #js {:lineNumbers false 70 | :undoDepth 100000000 71 | :historyEventDelay 1 72 | :viewportMargin js/Infinity 73 | :autofocus true 74 | :readOnly read-only? 75 | :value @value-atom 76 | :theme "eclipse" ; or "monokai" 77 | :autoCloseBrackets true 78 | :hintOptions #js {:hint (partial autocomplete indexes) 79 | :completeSingle false} 80 | :extraKeys {"Ctrl-Space" "autocomplete"} ;need to leave this in for `:` to work, there's probably a better way! 81 | :matchBrackets true 82 | :mode "clojure"} 83 | inst (codemirror. el opts)] 84 | (.on inst "keyup" 85 | (fn [cm e] (when (and (not (gobj/getValueByKeys cm #js ["state" "completionActive"])) 86 | (= 1 (-> (gobj/get e "key") (count))) 87 | (= (gobj/get e "key") ":")) 88 | (.showHint inst)))) 89 | (reset! cm-inst inst) 90 | (.on inst "change" 91 | (fn [] 92 | (let [value (.getValue inst)] 93 | (when-not (= value @value-atom) 94 | (on-change value) 95 | (reset! value-atom value))))) 96 | (when on-cm-init 97 | (on-cm-init inst)))) 98 | 99 | :component-did-update 100 | (fn [this old-argv] 101 | (when-not (= @value-atom (.getValue @cm-inst)) 102 | (.setValue @cm-inst @value-atom) 103 | ;; reset the cursor to the end of the text, if the text was changed externally 104 | (let [last-line (.lastLine @cm-inst) 105 | last-ch (count (.getLine @cm-inst last-line))] 106 | (.setCursor @cm-inst last-line last-ch)))) 107 | 108 | :reagent-render 109 | (fn [_ _ _] 110 | [:div.code-mirror-container 111 | [:style code-mirror-styling]])}))) 112 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/contenteditable.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.contenteditable 2 | (:require [reagent.core :as r] 3 | [crux-ui.views.commons.user-intents :as user-intents])) 4 | 5 | 6 | (defn- -data-attrs-mapper [[k v]] 7 | (vector (str "data-" (name k)) v)) 8 | 9 | (defn- render-data-attrs [hmap] 10 | (into {} (map -data-attrs-mapper hmap))) 11 | 12 | 13 | (defn div 14 | [id 15 | {:keys 16 | [on-change 17 | on-intent 18 | on-key-down 19 | on-blur 20 | on-change-complete 21 | text-mode? 22 | process-paste] 23 | :as opts}] 24 | ;; 25 | (let [node (r/atom nil) 26 | state-value (atom nil) 27 | 28 | get-cur-value 29 | (fn [] 30 | (when-let [n @node] 31 | (if text-mode? 32 | (.-textContent n) 33 | (.-innerHTML n)))) 34 | 35 | on-key-down (cond 36 | on-intent #(some-> % user-intents/key-down-evt->intent-evt on-intent) 37 | on-key-down on-key-down 38 | :else identity) 39 | 40 | on-key-up-internal 41 | (if on-change 42 | (fn [evt] 43 | (let [cur-val (get-cur-value)] 44 | (when (not= cur-val @state-value) 45 | (on-change {:value cur-val 46 | :target @node}))))) 47 | 48 | on-blur-internal 49 | (fn [evt] 50 | (if on-blur (on-blur)) 51 | (if on-change-complete 52 | (on-change-complete 53 | {:value (get-cur-value) 54 | :target @node}))) 55 | 56 | on-paste-internal 57 | (if on-change 58 | (fn [evt] 59 | (let [cur-val (get-cur-value)] 60 | (when (not= cur-val @state-value) 61 | (let [paste-processed (if process-paste 62 | (process-paste cur-val) 63 | cur-val)] 64 | (on-change {:value paste-processed 65 | :target @node}))))))] 66 | (r/create-class 67 | {:display-name "ContentEditable" 68 | 69 | :component-did-mount 70 | (fn [this] 71 | (reset! node (r/dom-node this))) 72 | 73 | :should-component-update 74 | (fn [this cur-argv [f id next-props :as next-argv]] 75 | (let [active-element js/document.activeElement 76 | not-active? (not= @node active-element)] 77 | (and not-active? (not= (get-cur-value) (:value next-props))))) 78 | 79 | :reagent-render 80 | (fn [id {:keys [data ; @param {map} with data attributes 81 | on-blur on-focus 82 | placeholder css-class value] :as opts}] ;; remember to repeat parameters 83 | (reset! state-value value) 84 | (let [id-str (if (keyword? id) (name id) (str id)) 85 | data-attrs (render-data-attrs data)] 86 | [:div.crux-ui-input.contenteditable 87 | (cond-> 88 | {:id id-str, 89 | :suppressContentEditableWarning true 90 | :dangerouslySetInnerHTML {:__html value} 91 | :placeholder placeholder 92 | :class (if css-class (name css-class)) 93 | :autoFocus (:autofocus opts) 94 | :content-editable true 95 | :spellCheck "false" 96 | :on-key-up on-key-up-internal 97 | :on-paste on-paste-internal 98 | :on-key-down on-key-down 99 | :on-focus on-focus 100 | :on-blur on-blur-internal} 101 | data-attrs (merge data-attrs))]))}))) 102 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/css_cube.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.css-cube 2 | (:require [crux-ui.views.functions :as vf] 3 | [garden.core :as garden])) 4 | 5 | (def ^:private cube-styles 6 | [:style 7 | (garden/css 8 | [:.cube 9 | {:display :block 10 | ;:outline "1px solid blue" 11 | :height :50px 12 | :position :relative 13 | :width :50px 14 | :transform-origin "50% 50% -25px" 15 | :transform-style "preserve-3d" 16 | :transform "rotateX(-45deg) rotateY(55deg)"} 17 | (let [rib-length "100%" 18 | rib-thickness "1px" 19 | transform-back "translateZ(-50px)" 20 | transform-left "rotateY(90deg)" 21 | transform-right "rotateY(-90deg)"] 22 | [:&__rib 23 | {:width rib-length 24 | :height rib-thickness 25 | :background :black 26 | :position :absolute} 27 | [:&--center 28 | {:height rib-length 29 | :width rib-thickness 30 | :bottom 0}] 31 | [:&--bottom 32 | {:bottom 0}] 33 | [:&--left 34 | {:right 0 35 | :transform-origin "0%" 36 | :transform transform-left}] 37 | [:&--right 38 | {:right 0 39 | :transform-origin "100%" 40 | :transform transform-right}] 41 | [:&--top&--right 42 | {:background :orange}] 43 | [:&--center&--right 44 | :&--center&--left 45 | {:transform :none}] 46 | [:&--back 47 | :&--back&--center 48 | {:transform transform-back}] 49 | [:&--center&--back 50 | {}]])])]) 51 | 52 | (defn rib [& modifiers] 53 | [:div (apply vf/bem (cons :cube__rib modifiers))]) 54 | 55 | (defn crux-cube [] 56 | [:div.cube 57 | cube-styles 58 | [rib :top :front] 59 | [rib :top :left] 60 | [rib :top :back] 61 | [rib :top :right] 62 | ; 63 | [rib :center :left] 64 | [rib :center :right] 65 | [rib :center :back :left] 66 | [rib :center :back :right] 67 | ; 68 | [rib :bottom :front] 69 | [rib :bottom :left] 70 | [rib :bottom :back] 71 | [rib :bottom :right]]) 72 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/css_logo.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.css-logo 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [clojure.string :as s] 5 | [crux-ui.views.commons.cube-svg :as cube] 6 | [crux-ui.views.functions :as vf])) 7 | 8 | 9 | (def ^:private css-logo-styles 10 | [:style 11 | (garden/css 12 | [:.css-logo 13 | {:display :flex 14 | :justify-content :space-between 15 | :align-items :center 16 | :width "100%"} 17 | [:&__cube 18 | {:width :60px 19 | :height :60px 20 | :flex "0 0 60px" 21 | :margin-right :8px} 22 | [:>.svg-cube 23 | {:height :100% 24 | :width :83.3%}]] 25 | [:&__text 26 | {:display "flex" 27 | :justify-content "space-between" 28 | :letter-spacing "0.15em" 29 | :font-weight "400" 30 | :flex "0 0 250px" 31 | :align-items "center"}]] 32 | (gs/at-media {:max-width :500px} 33 | [:.css-logo__text 34 | {:display :none}]) 35 | (gs/at-media {:max-width :375px} 36 | [:.css-logo__cube 37 | {:width :40px 38 | :height :40px 39 | :flex "0 0 40px"}]))]) 40 | 41 | 42 | (defn root [] 43 | [:div.css-logo 44 | css-logo-styles 45 | [:div.css-logo__cube (cube/cube {:animating? false})] 46 | [:div.css-logo__text "[?console]"]]) 47 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/cube_svg.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.cube-svg 2 | (:require [garden.stylesheet :as gs] 3 | [garden.core :as garden] 4 | [clojure.string :as s] 5 | [crux-ui.views.functions :as vu])) 6 | 7 | 8 | (def color-gray "rgb(201, 201, 201)") 9 | (def color-black "rgb(1, 1, 1)") 10 | 11 | 12 | (def kf-pulse-gray-orange 13 | (let [frame1 14 | {:stroke color-gray 15 | :fill color-gray}] 16 | (gs/at-keyframes :pulse-gray-orange 17 | [:0% frame1] 18 | [:33% {:stroke :orange 19 | :fill :orange}] 20 | [:66% frame1]))) 21 | 22 | (def kf-pulse-black-orange 23 | (let [frame1 24 | {:stroke color-black 25 | :fill color-black}] 26 | (gs/at-keyframes :pulse-black-orange 27 | [:0% frame1] 28 | [:33% {:stroke :orange 29 | :fill :orange}] 30 | [:66% frame1] 31 | [:100% frame1]))) 32 | 33 | (def cube-animation 34 | (let [anim-for-gray "pulse-gray-orange" 35 | anim-for-black "pulse-black-orange" 36 | duration-ms 1000 37 | 38 | ms #(str % "ms") 39 | 40 | ae 41 | (fn [anim-name animation-order] 42 | (let [delay-ms (* (/ duration-ms 3) animation-order)] 43 | (s/join " " 44 | [anim-name 45 | (ms duration-ms) 46 | (ms delay-ms) 47 | "infinite"]))) 48 | 49 | animated 50 | (list 51 | [:.rib--grey.rib--bottom-left 52 | {:animation (ae anim-for-gray 0)}] 53 | [:.rib--bottom-front 54 | {:animation (ae anim-for-black 0)}] 55 | [:.rib--bottom-right 56 | {:animation (ae anim-for-black 0)}] 57 | [:.rib--grey.rib--bottom-back 58 | {:animation (ae anim-for-gray 0)}] 59 | 60 | 61 | [:.rib--grey.rib--center-back-left 62 | {:animation (ae anim-for-gray 1)}] 63 | [:.rib--black.rib--center-right 64 | {:animation (ae anim-for-black 1)}] 65 | 66 | 67 | [:.rib--top-front 68 | :.rib--top-back 69 | :.rib--top-left 70 | :.rib--orange.rib--top-right 71 | {:animation (ae anim-for-black 2)}])] 72 | animated)) 73 | 74 | (def style 75 | [:style 76 | (let [stroke-width--desktop1 "15px" 77 | stroke-width--desktop2 "6px" 78 | ; 79 | stroke-width--mobile1 "21px" 80 | stroke-width--mobile2 "15px" 81 | ; 82 | stroke-color--grey "rgb(201, 201, 201)" 83 | stroke-color--orange "rgb(248, 150, 29)" 84 | stroke-color--black "#000"] 85 | 86 | (garden/css 87 | kf-pulse-black-orange 88 | kf-pulse-gray-orange 89 | 90 | [:.svg-cube 91 | {:width :83.3% 92 | :height :100%} 93 | [:&--animating 94 | cube-animation]] 95 | [:.rib 96 | {}] 97 | [:.rib--grey 98 | {:stroke stroke-color--grey 99 | :stroke-width stroke-width--desktop1 100 | :stroke-miterlimit "10px"}] 101 | [:.rib--black 102 | {:stroke stroke-color--black 103 | :stroke-width stroke-width--desktop2}] 104 | [:.rib6 105 | {:stroke-width :12px}] 106 | [:.rib--orange 107 | {:stroke stroke-color--orange 108 | :stroke-width stroke-width--desktop2 109 | :fill stroke-color--orange}] 110 | ; 111 | (gs/at-media {:max-width :768px} 112 | [:.rib 113 | {}] 114 | [:.rib--grey 115 | {:stroke stroke-color--grey 116 | :stroke-width stroke-width--mobile1 117 | :stroke-miterlimit "10px"}] 118 | [:.rib--black 119 | {:stroke-width stroke-width--mobile2 120 | :stroke stroke-color--black}] 121 | [:.rib6 122 | {:stroke-width :22px}] 123 | [:.rib--orange 124 | {:stroke-width stroke-width--mobile2 125 | :stroke stroke-color--orange 126 | :fill stroke-color--orange}])))]) 127 | 128 | 129 | 130 | (defn cube [{:keys [animating?]}] 131 | [:svg 132 | {:class (vu/bem-str :svg-cube {:animating animating?}) 133 | :version "1.1" :x "0px" :y "0px" :viewBox "0 0 500 598.837" 134 | :xmlns "http://www.w3.org/2000/svg"} 135 | style 136 | [:line.rib.rib--grey.rib--bottom-left 137 | {:x1 "250.911" :y1 "249.156" :x2 "19.097" :y2 "421.962"}] 138 | [:line#line14.rib.rib--grey.rib--center-back-left 139 | {:x1 "249.528" :y1 "252.244" :x2 "250.555" :y2 "4.264"}] 140 | [:line.rib.rib--grey.rib--bottom-back 141 | {:x1 "250.649" :y1 "250.835" :x2 "483.864" :y2 "419.57"}] 142 | [:line.rib.rib--black.rib--center-right.rib6 143 | {:x1 "483.578" :y1 "419.286" :x2 "483.578" :y2 "174.755"}] 144 | [:polygon.rib.rib--black.rib--top-front 145 | {:points "16.193 181.85 247.778 351.14 249.908 352.694 249.956 352.646 249.956 343.685 249.908 343.685 16.193 172.844"}] 146 | [:polygon.rib.rib--black.rib--bottom-front 147 | {:points "16.199 427.682 246.538 591.662 248.655 593.164 248.704 593.118 248.704 584.438 248.655 584.438 16.199 418.959"}] 148 | [:polygon.rib.rib--black.rib--bottom-right 149 | {:points "248.299 583.672 484.859 415.128 487.034 413.583 487.084 413.631 487.084 422.553 487.034 422.553 248.299 592.638" 150 | :transform "matrix(-1, 0, 0, -1, 735.383011, 1006.221008)"}] 151 | [:polygon.rib.rib--black.rib--top-back 152 | {:points "251.976 11.002 485.873 178.546 488.041 180.087 488.091 180.039 488.091 171.148 488.041 171.148 252.95 3.762" 153 | :transform "matrix(-1, 0, 0, -1, 738.143997, 181.72001)"}] 154 | [:polygon.rib.rib--black.rib--top-left 155 | {:points "16.268 171.283 247.892 2.993 250.021 1.45 250.07 1.498 250.07 10.405 250.021 10.405 16.268 180.236"}] 156 | [:polygon.rib.rib--orange.rib--top-right 157 | {:points "249.956 343.562 250.005 343.562 250.005 352.646 486.649 180.05 486.535 170.226"}]]) 158 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/datepicker_native.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.datepicker-native 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [crux-ui.logging :as log] 5 | [crux-ui.functions :as f] 6 | [crux-ui.logic.time :as time] 7 | [crux-ui.config :as cfg] 8 | [crux-ui.views.commons.input :as input] 9 | [crux-ui.views.commons.keycodes :as kc] 10 | [reagent.core :as r] 11 | [crux-ui.views.commons.dom :as dom] 12 | [crux-ui.views.functions :as vu])) 13 | 14 | 15 | (defn- on-time-change--native [on-change-external evt] 16 | (try 17 | (let [v (f/jsget evt "target" "value") 18 | ts (js/Date.parse v)] 19 | (if (js/isNaN ts) 20 | (on-change-external nil) 21 | (on-change-external (js/Date. ts)))) 22 | (catch js/Error err 23 | (on-change-external nil) 24 | (log/error err)))) 25 | 26 | 27 | (def style 28 | [:style 29 | (garden/css 30 | [:.native-date-time-picker 31 | {:display :flex 32 | :font-size :14px 33 | :align-items :center} 34 | [:&__label 35 | {:width :136px 36 | :display :block 37 | :letter-spacing :.04em}] 38 | [:&__input 39 | input/styles-src 40 | {:padding "4px 0" 41 | :width :auto}]] 42 | (gs/at-media {:min-width :1000px} 43 | [:.native-date-time-picker--column 44 | {:flex-direction :column 45 | :align-items :flex-start} 46 | ["> .native-date-time-picker__label" 47 | {:font-size :16px 48 | :width :auto 49 | :letter-spacing :0.09em 50 | :color :gray}] 51 | ["> .native-date-time-picker__input" 52 | {:line-height :32px}]]))]) 53 | 54 | 55 | 56 | (defn picker 57 | [{:keys 58 | [label 59 | ui/layout 60 | ^js/Date value 61 | on-change] 62 | :as prms}] 63 | (let [state (r/atom {:value value}) 64 | on-commit-internal (r/partial on-time-change--native on-change)] 65 | (fn [] 66 | [:div (vu/bem :native-date-time-picker layout) 67 | (if label 68 | [:label.native-date-time-picker__label label]) 69 | [:input.native-date-time-picker__input 70 | (let [v (:value @state)] 71 | {:type "datetime-local" 72 | :placeholder (if-not cfg/supports-input-datetime? "dd/mm/yyyy, --:--") 73 | :defaultValue (if v (time/format-for-dt-local v)) 74 | :on-key-down 75 | (dom/dispatch-on-keycode 76 | {::kc/enter on-commit-internal}) 77 | :on-blur on-commit-internal})]]))) 78 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/form_line.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.form-line 2 | (:require [clojure.string :as s])) 3 | 4 | (defn line [{:keys [on-reset css-class label control hint]}] 5 | [:div.line {:class css-class :title label} 6 | (if label 7 | [:div.line__label label]) 8 | [:div.line__control control 9 | (if on-reset 10 | [:button.line__reset 11 | {:title (if-not label 12 | "reset" 13 | (s/join " " (cons "reset" (filter string? label)))) 14 | :on-click on-reset} "x"])] 15 | (if hint 16 | [:div.line__hint hint])]) 17 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/input.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.input 2 | (:require [reagent.core :as r] 3 | [crux-ui.views.commons.user-intents :as user-intents] 4 | [garden.core :as garden] 5 | [crux-ui.views.style :as s])) 6 | 7 | 8 | (defn- -data-attrs-mapper [[k v]] 9 | (vector (str "data-" (name k)) v)) 10 | 11 | (defn render-data-attrs [hmap] 12 | (into {} (map -data-attrs-mapper hmap))) 13 | 14 | 15 | (def styles-src 16 | (list 17 | {:border :none} 18 | {:border-bottom s/q-ui-border} 19 | {:padding "6px 0px" 20 | :border-radius :2px 21 | :width :100% 22 | :letter-spacing :.09em 23 | :outline :none 24 | :font-size :inherit 25 | :font-family :inherit 26 | :display :inline-block} 27 | ["&:empty" 28 | {:min-width :220px}] 29 | ["&:empty::before" 30 | {:content "attr(placeholder)"}])) 31 | 32 | (def styles 33 | [:.crux-ui-input styles-src]) 34 | 35 | 36 | (defn text 37 | [id {:keys [on-change on-change-complete 38 | on-intent on-key-down 39 | parse-fn 40 | value 41 | process-paste] 42 | :as opts}] 43 | (let [node (r/atom nil) 44 | cur-val (atom value) 45 | parse-fn (or parse-fn identity) 46 | get-cur-value #(some-> @node (.-value) parse-fn) 47 | on-key-down (cond 48 | on-intent #(some-> % user-intents/key-down-evt->intent-evt on-intent) 49 | on-key-down on-key-down 50 | :else identity) 51 | 52 | on-blur-internal 53 | (fn [evt] 54 | (if on-change-complete 55 | (on-change-complete {:value (get-cur-value)}))) 56 | 57 | on-key-up-internal 58 | (if on-change 59 | (fn [evt] 60 | (let [cur-value (get-cur-value)] 61 | (when (not= cur-value @cur-val) 62 | (on-change {:value cur-value 63 | :target @node}))))) 64 | 65 | on-paste-internal 66 | (if on-change 67 | (fn [evt] 68 | (let [cur-html (get-cur-value)] 69 | (when (not= cur-html @cur-val) 70 | (let [paste-processed (if process-paste 71 | (process-paste cur-html) 72 | cur-html)] 73 | (on-change {:value (parse-fn paste-processed) 74 | :target @node}))))))] 75 | (r/create-class 76 | {:display-name "SpaceInput" 77 | 78 | :component-did-mount 79 | (fn [this] 80 | (reset! node (r/dom-node this))) 81 | 82 | :component-did-update 83 | (fn [this old-argv] 84 | (let [nv (:value (r/props this))] 85 | (reset! cur-val nv))) 86 | 87 | :reagent-render 88 | (fn [id {:keys [data ; @param {map} with data attributes 89 | on-blur on-focus 90 | placeholder css-class value] :as opts}] ;; remember to repeat parameters 91 | (let [id-str (if (keyword? id) (name id) (str id)) 92 | attrs 93 | (cond-> {:id id-str 94 | :placeholder placeholder 95 | :class (if css-class (name css-class)) 96 | :defaultValue value 97 | :autoFocus (:autofocus opts) 98 | :spellCheck "false" 99 | :on-key-up on-key-up-internal 100 | :on-paste on-paste-internal 101 | :on-key-down on-key-down 102 | :on-focus on-focus 103 | :on-blur on-blur-internal} 104 | (seq data) (merge (render-data-attrs data)))] 105 | 106 | ^{:key value} 107 | [:input.crux-ui-input attrs]))}))) 108 | 109 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/keycodes.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.keycodes) 2 | 3 | (def kc->kw 4 | {3 ::break 5 | 8 ::backspace-or-delete 6 | 9 ::tab 7 | 12 ::clear 8 | 13 ::enter 9 | 16 ::shift 10 | 17 ::ctrl 11 | 18 ::alt 12 | 19 ::pause-break 13 | 20 ::caps-lock 14 | 21 ::hangul 15 | 25 ::hanja 16 | 27 ::escape 17 | 28 ::conversion 18 | 29 ::non-conversion 19 | 32 ::spacebar 20 | 33 ::page-up 21 | 34 ::page-down 22 | 35 ::end 23 | 36 ::home 24 | 37 ::left-arrow 25 | 38 ::up-arrow 26 | 39 ::right-arrow 27 | 40 ::down-arrow 28 | 41 ::select 29 | 42 ::print 30 | 45 ::insert 31 | 46 ::delete 32 | 47 ::help 33 | 48 ::0 34 | 49 ::1 35 | 50 ::2 36 | 51 ::3 37 | 52 ::4 38 | 53 ::5 39 | 54 ::6 40 | 55 ::7 41 | 56 ::8 42 | 57 ::9 43 | 65 ::a 44 | 66 ::b 45 | 67 ::c 46 | 68 ::d 47 | 69 ::e 48 | 70 ::f 49 | 71 ::g 50 | 72 ::h 51 | 73 ::i 52 | 74 ::j 53 | 75 ::k 54 | 76 ::l 55 | 77 ::m 56 | 78 ::n 57 | 79 ::o 58 | 80 ::p 59 | 81 ::q 60 | 82 ::r 61 | 83 ::s 62 | 84 ::t 63 | 85 ::u 64 | 86 ::v 65 | 87 ::w 66 | 88 ::x 67 | 89 ::y 68 | 90 ::z 69 | 70 | 91 ::meta--left 71 | 92 ::right-window-key 72 | 93 ::meta--right 73 | 96 ::numpad-0 74 | 97 ::numpad-1 75 | 98 ::numpad-2 76 | 99 ::numpad-3 77 | 100 ::numpad-4 78 | 101 ::numpad-5 79 | 102 ::numpad-6 80 | 103 ::numpad-7 81 | 104 ::numpad-8 82 | 105 ::numpad-9 83 | 106 ::multiply 84 | 107 ::add 85 | 109 ::subtract 86 | 110 ::decimal-point 87 | 111 ::divide 88 | 112 ::f1 89 | 113 ::f2 90 | 114 ::f3 91 | 115 ::f4 92 | 116 ::f5 93 | 117 ::f6 94 | 118 ::f7 95 | 119 ::f8 96 | 120 ::f9 97 | 121 ::f10 98 | 122 ::f11 99 | 123 ::f12 100 | 124 ::f13 101 | 125 ::f14 102 | 126 ::f15 103 | 127 ::f16 104 | 128 ::f17 105 | 106 | 129 ::f18 107 | 130 ::f19 108 | 131 ::f20 109 | 132 ::f21 110 | 133 ::f22 111 | 134 ::f23 112 | 135 ::f24 113 | 144 ::num-lock 114 | 145 ::scroll-lock 115 | 166 ::page-backward 116 | 167 ::page-forward 117 | 168 ::refresh 118 | 172 ::home-key 119 | 187 ::equals 120 | 189 ::minus 121 | 174 ::decrease-volume-level 122 | 175 ::increase-volume-level 123 | 225 ::altgr}) 124 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/tabs.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.tabs 2 | (:require [garden.core :as garden] 3 | [reagent.core :as r])) 4 | 5 | (def tabs-styles 6 | [:style 7 | (garden/css 8 | [:.tabs 9 | {:display "flex" 10 | :justify-content "space-between" 11 | :letter-spacing ".08em" 12 | :text-transform :uppercase 13 | :font-size :13px 14 | :align-items "center"} 15 | [:&__item 16 | {:cursor :pointer} 17 | [:&--active 18 | {:font-weight 600}]] 19 | [:&__sep 20 | {:padding "0 8px"}] 21 | [:&--header 22 | {:font-size "20px"} 23 | [:>.tabs__sep 24 | {:padding "16px"}]]])]) 25 | 26 | 27 | (defn root [{:tabs/keys [tabs on-tab-activate active-id]}] 28 | [:div.tabs 29 | tabs-styles 30 | (butlast 31 | (interleave 32 | (for [{:tabs/keys [id title href] :as tab} tabs] 33 | ^{:key id} 34 | [:a.tabs__item.g-nolink 35 | (cond-> 36 | {:class (if (= id active-id) "tabs__item--active")} 37 | href (assoc :href href) 38 | (not href) (assoc :on-click (r/partial on-tab-activate id))) 39 | title]) 40 | (map (fn [i] ^{:key i} [:div.tabs__sep "/"]) (range))))]) 41 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/tiny_components.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.tiny-components 2 | (:require [re-frame.core :as rf] 3 | [crux-ui.views.functions :as vu] 4 | [garden.core :as garden] 5 | [crux-ui.svg-icons :as icon] 6 | [crux-ui.views.style :as s])) 7 | 8 | 9 | (defn icon' [icon-name] 10 | [:i.icon {:key icon-name, :class (str "icon-" (name icon-name))}]) 11 | 12 | (defn link-mailto [email] 13 | [:a {:href (str "mailto:" email)} email]) 14 | 15 | (defn link-outer [href title] 16 | [:a {:target "_blank" :href href :title (str "Open " title " in a new tab")} title #_icon/external]) 17 | 18 | (defn link-box [{:keys [main? luminous? active? svg? round? icon icon-alt on-click class href label attrs] :as params}] 19 | (let [css-class 20 | (-> :link 21 | (vu/bem-str {:main main? :round round? :active active? :luminous luminous?}) 22 | (str class))] 23 | [:a.link.link--box.g-nolink (merge attrs {:href href :title icon-alt :on-click on-click :class css-class}) 24 | #_(if icon 25 | (if svg? 26 | [svg/icon icon {:active? active?}] 27 | [:div.link__icon {:title icon-alt} [icon' icon icon-alt]])) 28 | (if label 29 | [:div.link__label label])])) 30 | 31 | (def col-base {:h 197 :s 80 :l 65 :a 0.8}) 32 | 33 | ; cta – call to action 34 | (def btn-color--cta (s/hsl col-base)) 35 | (def btn-color--inactive (s/hsl (assoc col-base :a 0.5))) 36 | (def btn-color--cta-hover (s/hsl (assoc col-base :a 0.9))) 37 | (def btn-color--cta-active (s/hsl (assoc col-base :a 1))) 38 | 39 | 40 | (def button-styles 41 | [:.button 42 | {:cursor :pointer 43 | :background :none 44 | :padding "6px 8px" 45 | :border-radius :2px 46 | :user-select :none 47 | :border "1px solid hsl(0, 0%, 85%)"} 48 | 49 | ; bordered cta button 50 | [:&--bright 51 | {:border (str "1px solid " btn-color--cta-active) 52 | :color btn-color--cta-active} 53 | [:&:hover 54 | {:border (str "1px solid " btn-color--cta-hover)}]] 55 | [:&--bright.button--inactive 56 | {:border (str "1px solid " btn-color--inactive) 57 | :color btn-color--inactive}] 58 | 59 | [:&--textual 60 | {:border :none 61 | :background :none}] 62 | [:&--active 63 | {:font-weight 400}] 64 | 65 | [:&--cta 66 | {:background btn-color--cta 67 | :border-radius :2px 68 | :color "white" 69 | :cursor :pointer 70 | :border 0 71 | :letter-spacing "0.03em" 72 | :padding "8px 14px"} 73 | [:&:hover 74 | {:background btn-color--cta-hover}] 75 | [:&:active 76 | {:background btn-color--cta-active}]] 77 | [:&--cta.button--inactive 78 | {:background btn-color--inactive}]]) 79 | 80 | (defn- button [main-mod {:keys [on-click active? text css-mods] :as params}] 81 | [:button 82 | {:type :button 83 | :on-click on-click 84 | :class (vu/bem-str :button main-mod css-mods {:active active?})} 85 | (if (:icon params) 86 | [:div.button__icon [icon' (:icon params)]]) 87 | [:span.button__text text]]) 88 | 89 | (def button-textual (partial button :textual)) 90 | (def button-cta (partial button :cta)) 91 | (def button-bordered (partial button :bordered)) 92 | -------------------------------------------------------------------------------- /src/crux_ui/views/commons/user_intents.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.commons.user-intents 2 | (:require [crux-ui.views.commons.keycodes :as kc] 3 | [crux-ui.views.commons.dom :as dom])) 4 | 5 | (defn interpret-user-intent [target-value key-code] 6 | (if (empty? target-value) 7 | (case (kc/kc->kw key-code) 8 | ::kc/enter ::create 9 | ::kc/backspace-or-delete ::delete 10 | nil) 11 | (case (kc/kc->kw key-code) 12 | ::kc/enter ::create 13 | ::kc/backspace-or-delete nil 14 | nil))) 15 | 16 | (defn key-down-evt->intent-evt 17 | "boolean altKey 18 | number charCode 19 | boolean ctrlKey 20 | boolean getModifierState(key) 21 | string key 22 | number keyCode 23 | string locale 24 | number location 25 | boolean metaKey 26 | boolean repeat 27 | boolean shiftKey 28 | number which" 29 | [react-evt] 30 | (let [t (.-target react-evt) 31 | v (or (not-empty (.-value t)) (.-innerHTML t)) 32 | window-selection (.getSelection js/window) 33 | kc (.-keyCode react-evt) 34 | intent (interpret-user-intent v kc)] 35 | (when intent 36 | (.preventDefault react-evt) 37 | {:intent intent 38 | :value v 39 | :target t 40 | :data (some-> t (dom/jsget "dataset") dom/dataset->clj-raw)}))) 41 | -------------------------------------------------------------------------------- /src/crux_ui/views/facade.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.facade 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [re-frame.core :as rf] 5 | [crux-ui.views.query-ui :as q] 6 | [crux-ui.views.header :as header] 7 | [crux-ui.views.commons.tiny-components :as comps] 8 | [crux-ui.views.settings :as settings] 9 | [crux-ui.svg-icons :as icon] 10 | [crux-ui.views.second-layer :as second-layer] 11 | [crux-ui.views.commons.input :as input] 12 | [crux-ui.functions :as f])) 13 | 14 | 15 | (def color-link "hsl(32, 61%, 64%)") 16 | (def color-link--hover "hsl(32, 91%, 54%)") 17 | 18 | (defn on-sidebar-bg-click [evt] 19 | (if (= "root__sidebar" (some-> evt (f/jsget "target" "id"))) 20 | (rf/dispatch [:evt.ui.second-layer/toggle]))) 21 | 22 | (def ^:private root-styles 23 | [:style 24 | (garden/css 25 | [[:a 26 | {:color color-link} 27 | [:&:visited {:color color-link}] 28 | [:&:hover {:color color-link--hover}]] 29 | [:h1 :h2 30 | {:font-weight 300 31 | :letter-spacing :0.19em}] 32 | [:h3 :h4 33 | {:font-weight 300 34 | :letter-spacing :0.05em}] 35 | [:.g-nolink 36 | :.g-nolink:active 37 | :.g-nolink:visited 38 | {:text-decoration :inherit 39 | :color :inherit}] 40 | [:button 41 | {:font-size :1rem}] 42 | input/styles 43 | icon/styles 44 | comps/button-styles 45 | [:html :body :#app 46 | {:font-family "Helvetica Neue, Helvetica, BlinkMacSystemFont, -apple-system, Roboto, 'Segoe UI', sans-serif" 47 | :height "100%" 48 | :font-weight 400}] 49 | 50 | [:.root 51 | {:display :grid 52 | :place-items :stretch 53 | :height :100% 54 | :grid-template 55 | "'header' 84px 56 | 'body' calc(100% - 84px)"} 57 | [:&--page 58 | {:grid-template 59 | "'header' 84px 60 | 'body' 1fr"}] 61 | [:&__header 62 | {:grid-area :header}] 63 | [:&__second-layer 64 | {:position :fixed 65 | :top :0px 66 | :bottom :0px 67 | :border-radius :2px 68 | :left :0px 69 | :right 0 70 | :z-index 100 71 | :background "hsla(0, 0%, 0%, .3)"}] 72 | [:&__body 73 | {:grid-area :body 74 | :display :flex 75 | :overflow :hidden 76 | :align-items :center 77 | :justify-content :center}]] 78 | 79 | (gs/at-media {:max-width :375px} 80 | [:h1 81 | {:font-size :30px 82 | :line-height 2.0 83 | :letter-spacing :0.08em 84 | :font-weight 400}] 85 | [:h2 86 | {:font-size :24px}]) 87 | 88 | (gs/at-media {:max-width :375px} 89 | [:.root 90 | {:grid-template 91 | "'header' 64px 92 | 'body' calc(100% - 64px)"}])])]) 93 | 94 | 95 | (defn root [] 96 | (let [-sub-root-tab (rf/subscribe [:subs.ui/root-tab]) 97 | -sub-second-layer (rf/subscribe [:subs.db.ui/second-layer])] 98 | (fn [] 99 | [:div#root.root 100 | root-styles 101 | [:div.root__header 102 | [header/root]] 103 | (if @-sub-second-layer 104 | [:div#root__second-layer.root__second-layer {:on-click on-sidebar-bg-click} 105 | [second-layer/root]]) 106 | [:div.root__body 107 | (case @-sub-root-tab 108 | :db.ui.root-tab/query-ui [q/query-ui] 109 | :db.ui.root-tab/settings [settings/root] 110 | [q/query-ui])]]))) 111 | -------------------------------------------------------------------------------- /src/crux_ui/views/facade_perf.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.facade-perf 2 | (:require [garden.core :as garden] 3 | [crux-ui.views.query-perf :as query-perf] 4 | [crux-ui.views.header :as header] 5 | [crux-ui.views.commons.tiny-components :as comps] 6 | [crux-ui.svg-icons :as icon])) 7 | 8 | (def ^:private root-styles 9 | [:style 10 | (garden/css 11 | [[:a 12 | {:color "hsl(32, 91%, 54%)"} 13 | [:&:visited {:color "hsl(32, 91%, 54%)"}]] 14 | [:button 15 | {:font-size :1rem}] 16 | icon/styles 17 | comps/button-styles 18 | [:html :body :#app 19 | {:font-family "Helvetica Neue, Helvetica, BlinkMacSystemFont, -apple-system, Roboto, 'Segoe UI', sans-serif" 20 | :height "100%" 21 | :font-weight 300}] 22 | 23 | [:.root 24 | {:display :grid 25 | :place-items :stretch 26 | :height "100%" 27 | :grid-template 28 | "'header' 100px 29 | 'body' calc(100% - 100px)"} 30 | [:&--page 31 | {:grid-template 32 | "'header' 100px 33 | 'body' 1fr"}] 34 | [:&__header 35 | {:grid-area :header}] 36 | [:&__body 37 | {:grid-area :body 38 | :display :flex 39 | :align-items :center 40 | :justify-content :center}]]])]) 41 | 42 | 43 | (defn root [] 44 | [:div#root.root.root--page 45 | root-styles 46 | [:div.root__header 47 | [header/root]] 48 | [:div.root__body.root__body--page 49 | [query-perf/root]]]) 50 | 51 | -------------------------------------------------------------------------------- /src/crux_ui/views/functions.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.functions) 2 | 3 | 4 | ; bem 5 | (defn unfold-modifier-maps [possible-modifier-map] 6 | (if-not (map? possible-modifier-map) 7 | possible-modifier-map 8 | (for [[k v] possible-modifier-map :when v] k))) 9 | 10 | (defn flatten-modifiers [modifiers] 11 | (flatten (map unfold-modifier-maps modifiers))) 12 | 13 | (defn- bem-str-strict [css-class-name modifiers] 14 | (let [base-class-name (name css-class-name) 15 | modifiers (flatten-modifiers modifiers) 16 | -append 17 | (fn [final-class-name modifier-name] 18 | (if modifier-name 19 | (str final-class-name " " base-class-name"--" (.replace (name modifier-name) "?" "")) 20 | final-class-name))] 21 | (reduce -append base-class-name modifiers))) 22 | 23 | (defn bem-str [css-class-name & modifiers] 24 | (bem-str-strict css-class-name modifiers)) 25 | 26 | (defn bem [css-class-name & modifiers] 27 | {:class (bem-str-strict css-class-name modifiers)}) 28 | 29 | -------------------------------------------------------------------------------- /src/crux_ui/views/header.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.header 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [re-frame.core :as rf] 5 | [crux-ui.routes :as routes] 6 | [crux-ui.views.commons.tabs :as tabs] 7 | [crux-ui.views.commons.css-logo :as css-logo] 8 | [crux-ui.views.node-status :as node-status] 9 | [crux-ui.views.commons.input :as input] 10 | [crux-ui.views.commons.tiny-components :as comps] 11 | [crux-ui.config :as cfg])) 12 | 13 | 14 | (def ^:private -sub-display-mode (rf/subscribe [:subs.ui/display-mode])) 15 | 16 | (defn disable-toggle-display-mode [] 17 | (rf/dispatch [:evt.ui.display-mode/toggle])) 18 | 19 | (defn dispatch-second-layer-toggle [] 20 | (rf/dispatch [:evt.ui.second-layer/toggle])) 21 | 22 | 23 | (def ^:private header-styles 24 | [:style 25 | (garden/css 26 | [:.header 27 | {:display :grid 28 | :grid-template "'logo status spacer links' / 184px 100px 1fr 260px" 29 | :place-items "center start" 30 | :padding "12px 1rem" 31 | :width "100%"} 32 | [:&__logo 33 | {:display :flex 34 | :grid-area :logo 35 | :justify-content :space-between 36 | :align-items :center}] 37 | [:&__status 38 | {:grid-area :status}] 39 | [:&__display-mode-toggle 40 | {:display :none}] 41 | [:&__links 42 | {:display "flex" 43 | :grid-area :links 44 | :place-self "center stretch" 45 | :justify-content "space-between" 46 | :flex "0 0 250px" 47 | :align-items "center"}]] 48 | (gs/at-media {:max-width :1000px} 49 | [:.header 50 | {:padding "16px" 51 | :grid-gap :8px 52 | :grid-template "'logo status toggle' / 1fr 1fr 1fr"} 53 | [:&__display-mode-toggle 54 | {:display :block 55 | :justify-self :end 56 | :grid-area :toggle}] 57 | [:&__links 58 | {:display :none}]]) 59 | (gs/at-media {:max-width :600px} 60 | [:.header 61 | {:padding "16px" 62 | :grid-template "'logo status toggle' / auto auto auto"}]) 63 | (gs/at-media {:max-width :400px} 64 | [:.header 65 | {:padding "16px 4px 16px 16px" 66 | :grid-gap :4px 67 | :grid-template "'logo status toggle' / auto minmax(auto, 120px) auto"} 68 | [:&__status 69 | {:justify-self :stretch 70 | :max-width :100%}]]))]) 71 | 72 | (defn root [] 73 | [:header.header 74 | header-styles 75 | [:div.header__logo {:on-click dispatch-second-layer-toggle} 76 | [css-logo/root]] 77 | [:div.header__status 78 | [node-status/node-status]] 79 | 80 | [:div.header__display-mode-toggle 81 | [comps/button-textual 82 | {:on-click disable-toggle-display-mode 83 | :text 84 | (if (= @-sub-display-mode :ui.display-mode/query) 85 | "To output >" 86 | "< To query")}]] 87 | 88 | [:div.header__links 89 | [:div.header__links__item 90 | [comps/link-outer cfg/url-docs "Docs"]] 91 | [:div.header__links__item 92 | [comps/link-outer cfg/url-chat "Crux Chat"]] 93 | [:div.header__links__item 94 | [comps/link-mailto cfg/url-mail]]]]) 95 | 96 | -------------------------------------------------------------------------------- /src/crux_ui/views/node_status.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.node-status 2 | (:require [re-frame.core :as rf] 3 | [garden.core :as garden] 4 | [garden.stylesheet :as gs] 5 | [crux-ui.views.commons.contenteditable :as editable] 6 | [crux-ui.functions :as f] 7 | [crux-ui.views.commons.keycodes :as kc] 8 | [crux-ui.views.functions :as vf] 9 | [crux-ui.views.style :as s] 10 | [crux-ui.views.commons.dom :as dom])) 11 | 12 | 13 | 14 | (def ^:private -sub-node-addr (rf/subscribe [:subs.sys/host])) 15 | (def ^:private -sub-node-status (rf/subscribe [:subs.sys.host/status])) 16 | 17 | (defn- on-host-change [{v :value :as change-complete-evt}] 18 | (rf/dispatch [:evt.db/host-change v])) 19 | 20 | (def ^:private node-styles 21 | [:style 22 | (garden/css 23 | [:.led 24 | (let [size :8px] 25 | {:height size 26 | :width size 27 | :background :grey 28 | :margin-right :8px 29 | :border-radius size}) 30 | [:&--on 31 | {:background :green}]] 32 | [:.node 33 | {:display :flex 34 | :position :relative 35 | :justify-content :space-between 36 | :align-items :center 37 | :width "100%"} 38 | [:>.node__status 39 | {:display :none}] 40 | [:&:hover 41 | [:>.node__status 42 | {:position :absolute 43 | :background :white 44 | :padding :8px 45 | ;:max-width :500px 46 | :font-family "monospace" 47 | :display :block 48 | :z-index 1000 49 | :font-size :1.2rem 50 | :border-radius :2px 51 | :border s/q-ui-border 52 | :top :32px}]] 53 | [:&__addr 54 | {:display "flex" 55 | :justify-content "space-between" 56 | :overflow :hidden 57 | :white-space :nowrap 58 | :text-overflow :ellipsis 59 | :flex "1 1 120px" 60 | :align-items "center"}]] 61 | (gs/at-media {:max-width :1000px} 62 | [".node>.node__status" 63 | {:display :none}]))]) 64 | 65 | 66 | (defn led [on?] 67 | [:div (vf/bem :led {:on on? :off (not on?)})]) 68 | 69 | (defn node-status [] 70 | [:div.node 71 | node-styles 72 | [:div.node__led [led (boolean @-sub-node-status)]] 73 | [:div.node__status 74 | (if-let [node-info @-sub-node-status] 75 | [:pre (f/pprint-str node-info)] ; alternatively [edn/root node-info] 76 | [:pre "Cannot connect to the specified node address"])] 77 | [:div.node__addr 78 | [editable/div ::host-input 79 | {:on-change-complete on-host-change 80 | :on-key-down 81 | (dom/dispatch-on-keycode 82 | {::kc/enter #(some-> % (.-target) (.blur))}) 83 | :placeholder "Node hostname/and-path" 84 | :value @-sub-node-addr}]]]) 85 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/attr_history.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.attr-history 2 | (:require [re-frame.core :as rf] 3 | [garden.core :as garden] 4 | [crux-ui.views.charts.wrapper-basic :as charts] 5 | [crux-ui.functions :as f] 6 | [crux-ui.views.commons.tiny-components :as comps])) 7 | 8 | (def ^:private -sub-plotly-data (rf/subscribe [:subs.query/attr-history-plot-data])) 9 | 10 | 11 | (def ^:private root-styles 12 | [:style 13 | (garden/css 14 | [:.attr-history 15 | {:height :100% 16 | :padding "6px 8px" 17 | :position :relative} 18 | [:&__chart 19 | {:height :100%}] 20 | [:&__hint 21 | {:position :absolute 22 | :background "hsla(0,0%, 100%, 0.9)" 23 | :top 0 24 | :left 0 25 | :bottom 0 26 | :right 0 27 | :overflow :hidden 28 | :display :grid 29 | :grid-template 30 | (f/lines 31 | "'h h h' 1fr" 32 | "'l hint r' auto" 33 | "'l btn r' auto" 34 | "'f f f' 1fr" 35 | "/ 1fr auto 1fr") 36 | :grid-gap :32px 37 | :place-items "center start" 38 | :width :100% 39 | :height :100%} 40 | ["> .q-output-empty" 41 | {:height :auto 42 | :grid-area :hint}] 43 | ["> .button" 44 | {:height :auto 45 | :grid-area :btn}]]])]) 46 | 47 | (def ^:private hint 48 | (str "This view composes `history-range` and `documents` endpoints to\n" 49 | "give you a some understanding of how an entity attribute has changed over valid time.\n" 50 | "This view is limited to display up to 7 entities.\n" 51 | "You can use \"put with valid time\" example to transact in\n" 52 | "some historical data for. Use that entity id later in your query.\n" 53 | "\n" 54 | "Try to run a query that will include a numeric attribute like:\n" 55 | "{:find [e p]\n" 56 | " :args [{ids #{:ids/tech-ticker-2 :ids/pharma-ticker-5}}]\n" 57 | " :where\n" 58 | " [[e :crux.db/id]\n" 59 | " [e :ticker/price p]\n" 60 | " [(contains? ids e)]]}\n")) 61 | 62 | (defn- attr-layout [attr] 63 | {:title (str (pr-str attr) " over time") 64 | :xaxis {:title "Valid Time"} 65 | :yaxis {:title (pr-str attr)}}) 66 | 67 | (defn- dispatch-disable-hint [_] 68 | (rf/dispatch [:evt.ui.attr-history/disable-hint])) 69 | 70 | (defn root [] 71 | [:div.attr-history 72 | root-styles 73 | (let [{:keys [traces attribute hint?] :as p-data} @-sub-plotly-data] 74 | (if p-data 75 | [:<> 76 | [:div.attr-history__chart 77 | [charts/plotly-wrapper traces (attr-layout attribute)]] 78 | (if hint? 79 | [:div.attr-history__hint 80 | [:pre.q-output-empty hint] 81 | [comps/button-bordered 82 | {:on-click dispatch-disable-hint 83 | :text "Got it"}]])] 84 | [:pre.q-output-empty hint]))]) 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/edn.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.edn 2 | (:require [cljs.pprint :as pp] 3 | [crux-ui.better-printer :as bp] 4 | [crux-ui.views.codemirror :as cm] 5 | [clojure.string :as s])) 6 | 7 | (defn root [edn] 8 | [cm/code-mirror (bp/better-printer edn) {:read-only? true}]) 9 | 10 | (defn simple-print [edn] 11 | [cm/code-mirror (bp/simple-print edn) {:read-only? true}]) 12 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/error.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.error 2 | (:require [cljs.pprint :refer [pprint]] 3 | [crux-ui.views.codemirror :as cm] 4 | [garden.core :as garden])) 5 | 6 | 7 | (def ^:private style 8 | [:style 9 | (garden/css 10 | [:.err-info 11 | {:padding "8px 16px" 12 | :height :100% 13 | :overflow :scroll} 14 | [:&__header 15 | {:line-height 1 16 | :padding "24px 8px 24px"}] 17 | [:&__title 18 | {:letter-spacing :0.09em 19 | :font-size :30px}] 20 | [:&__subtitle 21 | {:font-size :24px 22 | :margin-top :8px}]])]) 23 | 24 | 25 | 26 | (defn root [{:keys [query-type err/http-status err/type err/exception] :as err-data}] 27 | (let [fmt (with-out-str (pprint err-data))] 28 | [:div.err-info 29 | style 30 | [:div.err-info__header 31 | [:h1.err-info__title http-status " Error"] 32 | [:h2.err-info__subtitle 33 | (case type 34 | :err.type/client "Server considered the query malformed" 35 | :err.type/server "HTTP Server produced an error while executing the query")]] 36 | [:div.err-info__raw 37 | [cm/code-mirror fmt {:read-only? true}]]])) 38 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/react_tree.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.react-tree 2 | (:require ["react-ui-tree" :as ReactTree] 3 | [garden.core :as garden])) 4 | 5 | (defn on-tree-change [evt] 6 | (println :on-tree-change evt)) 7 | 8 | (def style 9 | [:style 10 | (garden/css 11 | [:.react-tree 12 | {:min-width :100px 13 | :min-height :100px 14 | :height :100% 15 | :overflow :auto 16 | :padding "40px 32px"}])]) 17 | 18 | (defn root [tree-struct] 19 | [:div.react-tree 20 | style 21 | [:> ReactTree 22 | {:paddingLeft 20 23 | :onChange on-tree-change 24 | :renderNode 25 | (fn [node] 26 | (.-title node)) 27 | 28 | :tree (clj->js tree-struct)}]]) 29 | 30 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/table.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.table 2 | (:require [re-frame.core :as rf] 3 | [crux-ui.views.style :as s] 4 | [garden.core :as garden] 5 | [reagent.core :as r] 6 | [crux-ui.functions :as f] 7 | [crux-ui.logging :as log] 8 | [crux-ui.views.commons.dom :as dom])) 9 | 10 | 11 | (def ^:private table-style 12 | [:style 13 | (garden/css 14 | 15 | [:.g-cell-oversize-arrest 16 | [:&__content 17 | {:max-width :300px 18 | :max-height :76px 19 | :overflow :hidden} 20 | [:&--expanded 21 | {:max-width :none 22 | :max-height :none}]] 23 | [:&__expand 24 | {:text-align :center 25 | :cursor :pointer}]] 26 | 27 | [:.q-grid-wrapper 28 | {:overflow :scroll 29 | :height :100% 30 | :padding-bottom :15rem} 31 | ["> ::-webkit-scrollbar" 32 | "> ::-moz-scrollbar" 33 | "> ::scrollbar" 34 | {:display :none}]] 35 | [:.q-grid 36 | {:border-collapse :separate 37 | :border-radius :2px 38 | :width :100% 39 | :overflow :visible 40 | :position :relative} 41 | [:&__head 42 | {} 43 | [:&-row 44 | {}]] 45 | 46 | ["&__body-cell" 47 | "&__head-cell" 48 | {:border-left s/q-ui-border 49 | :border-bottom s/q-ui-border 50 | :padding "10px 12px"}] 51 | ["&__head-cell" 52 | {:border-top :none 53 | :background :white 54 | :position :sticky 55 | :top 0 56 | :text-align :center 57 | :font-weight 400 58 | :letter-spacing :.10em} 59 | [:&:first-child 60 | {:border-left :none}]] 61 | [:&__body 62 | [:&-row 63 | {:border-top s/q-ui-border} 64 | [:&:first-child 65 | {:border-top :none}]] 66 | [:&-cell 67 | {:letter-spacing :.04em 68 | :border-bottom "1px solid hsl(200,20%, 90%)"} 69 | [:&:first-child 70 | {:border-left :none}] 71 | [:&--queryable 72 | {:cursor :pointer 73 | :box-shadow "inset 5px 0 0px -3px hsla(220, 90%, 70%, 0.5)"} 74 | [:&:hover 75 | {:box-shadow "inset 6px 0 0px -3px hsla(220, 90%, 70%, 1)"}]]]]])]) 76 | 77 | (defn- expand-sibling [click-evt] 78 | (let [cl (f/jsget click-evt "target" "previousSibling" "classList")] 79 | (^js .toggle cl "g-cell-oversize-arrest__content--expanded"))) 80 | 81 | (defn- on-queryable-cell-click [click-evt] 82 | (let [dataset (f/prefix-keys :data (dom/event->target-data click-evt))] 83 | (rf/dispatch [:evt.ui.query/submit--cell dataset]))) 84 | 85 | (defn- plain-value? [v] 86 | (or (boolean? v) 87 | (number? v) 88 | (keyword? v) 89 | (and (string? v) (< (.-length v) 100)))) 90 | 91 | ; (pr-str (type true)) 92 | 93 | (defn table-row [attr-headers value-is-the-attribute? i row-item-map] 94 | ^{:key i} 95 | [:tr.q-grid__body-row 96 | (for [attr attr-headers 97 | :let [raw-value (get row-item-map attr) 98 | cell-content ^js/String (or (some-> raw-value pr-str) "") 99 | cont-length (.-length cell-content) 100 | queryable? (and raw-value (plain-value? raw-value)) 101 | attr-str (pr-str (if value-is-the-attribute? raw-value attr))]] 102 | ^{:key attr} 103 | [:td.q-grid__body-cell 104 | 105 | (if queryable? 106 | (cond-> 107 | {:class (if queryable? "q-grid__body-cell--queryable") 108 | :on-click (if queryable? on-queryable-cell-click) 109 | :title attr-str 110 | :data-idx i 111 | :data-attr attr-str} 112 | ; 113 | (not value-is-the-attribute?) 114 | (assoc :data-value (pr-str raw-value)))) 115 | 116 | (if (> cont-length 100) 117 | [:div.g-cell-oversize-arrest 118 | [:div.g-cell-oversize-arrest__content cell-content] 119 | [:div.g-cell-oversize-arrest__expand 120 | {:on-click expand-sibling} "..."]] 121 | cell-content)])]) 122 | 123 | (defn root [table-data] 124 | (let [instance-state 125 | (r/atom {:i/node nil 126 | :i/ctrl-c? false}) 127 | 128 | on-ku-internal 129 | (fn [evt] 130 | (when-let [meta-released? (#{"Meta" "Control"} (.-key evt))] 131 | (swap! instance-state assoc :i/ctrl-c? false) 132 | (.remove (.-classList (:i/node @instance-state)) "q-grid--table"))) 133 | 134 | on-kd-internal 135 | (fn [kb-evt] 136 | (let [ctrl-or-cmd? (or (.-metaKey kb-evt) (.-ctrlKey kb-evt)) 137 | c? (= "c" (.-key kb-evt)) 138 | ctrl-c? (and ctrl-or-cmd? c?)] 139 | (when ctrl-c? 140 | (swap! instance-state assoc :i/ctrl-c? true) 141 | (.add (.-classList (:i/node @instance-state)) "q-grid--table"))))] 142 | 143 | (r/create-class 144 | {:component-will-unmount 145 | (fn [this] 146 | (js/window.removeEventListener "keydown" on-kd-internal true) 147 | (js/window.removeEventListener "keyup" on-ku-internal true)) 148 | :component-did-mount 149 | (fn [this] 150 | (let [node (r/dom-node this)] 151 | (js/window.addEventListener "keydown" on-kd-internal true) 152 | (js/window.addEventListener "keyup" on-ku-internal true) 153 | (swap! instance-state assoc :i/node node))) 154 | 155 | :reagent-render 156 | (fn [{:keys [headers attr-headers rows value-is-the-attribute?] :as table-data}] 157 | [:div.q-grid-wrapper 158 | [:table.q-grid 159 | table-style 160 | [:thead.q-grid__head 161 | [:tr.q-grid__head-row 162 | (for [[h ha] (map vector headers attr-headers)] 163 | ^{:key h} 164 | [:th.q-grid__head-cell {:title ha} (pr-str h)])]] 165 | [:tbody.q-grid__body 166 | (map-indexed (partial table-row attr-headers value-is-the-attribute?) rows)]]])}))) 167 | 168 | 169 | -------------------------------------------------------------------------------- /src/crux_ui/views/output/tx_history.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.output.tx-history 2 | (:require [re-frame.core :as rf] 3 | [garden.core :as garden] 4 | [crux-ui.views.charts.wrapper-basic :as charts])) 5 | 6 | (def ^:private -sub-tx-history (rf/subscribe [:subs.output/tx-history-plot-data])) 7 | 8 | (def ^:private root-styles 9 | [:style 10 | (garden/css 11 | [:.tx-history 12 | {:height :100% 13 | :padding "6px 8px"}])]) 14 | 15 | (def tx-layout 16 | {:title "Queried entities transactions" 17 | :xaxis {:title "Valid Time"} 18 | :yaxis {:title "Transaction time"}}) 19 | 20 | (defn root [] 21 | [:div.tx-history 22 | root-styles 23 | (if-let [tx-history @-sub-tx-history] 24 | [charts/plotly-wrapper tx-history tx-layout] 25 | [:div.q-output-empty "No data to display, try to run a query"])]) 26 | -------------------------------------------------------------------------------- /src/crux_ui/views/overview.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.overview 2 | (:require [crux-ui.views.settings :as settings] 3 | [crux-ui.views.sidebar :as sidebar] 4 | [crux-ui.views.commons.tiny-components :as comps] 5 | [garden.core :as garden] 6 | [garden.stylesheet :as gs] 7 | [re-frame.core :as rf] 8 | [crux-ui.config :as cfg])) 9 | 10 | (def ^:private root-styles 11 | [:style 12 | (garden/css 13 | [:.overview 14 | {:padding "16px 32px" 15 | :width :100%} 16 | [:label 17 | {:display :inline-block 18 | :width :200px}] 19 | [:.g-mt-2 20 | {:margin-top :16px}] 21 | [:kbd 22 | {:padding "4px 6px" 23 | :background :white 24 | :border-radius :2px 25 | :box-shadow "0 0px 0px 1px hsl(0, 0%, 75%)"}] 26 | [:&__section 27 | {:margin-top :32px}] 28 | [:&__footer 29 | {:margin-top :48px}]] 30 | (gs/at-media {:max-width :1000px} 31 | [:.overview 32 | {} 33 | [:&__section--shortcuts 34 | {:display :none}]]))]) 35 | 36 | (defn close [] 37 | (rf/dispatch [:evt.ui.second-layer.main-pane/cancel])) 38 | 39 | (defn root [] 40 | [:div.overview 41 | root-styles 42 | [:h1.overview__title "Console Overview"] 43 | [:section.overview__section.overview__section--shortcuts 44 | [:h2.overview__header "Features"] 45 | [:section.overview__section 46 | [:h3.overview__header "Attribute history"]] 47 | [:section.overview__section 48 | [:h3.overview__header "Examples import"] 49 | [:a {:href cfg/url-examples-gist :target :_blank} "Import example"]]] 50 | [:section.overview__section 51 | [:h2.overview__header "Limitations"] 52 | [:div.g-mt-2 "When you execute a normal query (not tx) not longer than 2000 53 | characters it will be placed into your address bar so you can share it."]] 54 | [:section.overview__section.overview__section--shortcuts 55 | [:h2.overview__header "Shortcuts"] 56 | [:div.g-mt-2 [:label "Query submit"] [:kbd "ctrl + enter"]] 57 | [:div.g-mt-2 [:label "Toggle editor"] [:kbd "ctrl + e"]] 58 | [:div.g-mt-2 [:label "Toggle fullscreen"] [:kbd "ctrl + cmd + f"]]] 59 | [:footer.overview__footer 60 | [comps/button-textual 61 | {:text "Cancel" 62 | :on-click close}]]]) 63 | -------------------------------------------------------------------------------- /src/crux_ui/views/query/datepicker_slider.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query.datepicker-slider 2 | (:require [re-frame.core :as rf] 3 | [crux-ui.logging :as log] 4 | [crux-ui.logic.time :as time] 5 | [crux-ui.views.commons.datepicker-native :as dpn] 6 | ["react-input-range" :as ir] 7 | [reagent.core :as r] 8 | [garden.core :as garden])) 9 | 10 | 11 | (def style 12 | [:style 13 | (garden/css 14 | [:.slider-date-time-picker 15 | {:width :100%} 16 | [:&__header 17 | {:display :flex 18 | :align-items :center 19 | :justify-content :space-between} 20 | [:&-label 21 | {:font-size :18px 22 | :font-weight 400 23 | :letter-spacing :0.09em}] 24 | [:&-native 25 | {:width :250px 26 | :flex "0 0 100px"}]] 27 | [:&__row 28 | {:display :flex 29 | :align-items :center 30 | :height :28px} 31 | [:&-label 32 | {:flex "0 0 60px" 33 | :text-transform :lowercase 34 | :font-size :13px}] 35 | [:&-slider 36 | {:flex "1 1 auto"}]]])]) 37 | 38 | 39 | (defn root-simple 40 | [{:keys [value ; components 41 | ^js/String label 42 | on-change 43 | on-change-complete] 44 | :as prms}] 45 | (let [on-change-internal 46 | (fn [time-component nv & [complete?]] 47 | (let [new-val (assoc value time-component nv)] 48 | (when (and complete? on-change-complete) 49 | (println :on-change (time/comps->date new-val)) 50 | (on-change-complete (time/comps->date new-val))) 51 | (when on-change 52 | (println :on-change (time/comps->date new-val)) 53 | (on-change (time/comps->date new-val)))))] 54 | [:div.slider-date-time-picker 55 | [:div.slider-date-time-picker__header 56 | [:label.slider-date-time-picker__header-label label] 57 | [:div.slider-date-time-picker__header-native 58 | [dpn/picker 59 | {:value (time/comps->date value) 60 | :on-change on-change}]]] 61 | [:div.slider-date-time-picker__row 62 | [:div.slider-date-time-picker__row-label "Year"] 63 | [:div.slider-date-time-picker__row-slider 64 | [:> ir {:value (get value :time/year) 65 | :step 1 66 | :classNames #js {:activeTrack "input-range__track input-range__track--active", 67 | :disabledInputRange "input-range input-range--disabled", 68 | :inputRange "input-range", 69 | :labelContainer "input-range__label-container", 70 | :maxLabel "input-range__label input-range__label--max", 71 | :minLabel "input-range__label input-range__label--min", 72 | :slider "input-range__slider", 73 | :sliderContainer "input-range__slider-container", 74 | :track "input-range__track input-range__track--background", 75 | :valueLabel "input-range__label input-range__label--value-year",} 76 | 77 | :minValue 1970 78 | :maxValue 2020 79 | :onChangeComplete #(on-change-internal :time/year % true) 80 | :onChange #(on-change-internal :time/year %)}]]] 81 | [:div.slider-date-time-picker__row 82 | [:div.slider-date-time-picker__row-label "Month"] 83 | [:div.slider-date-time-picker__row-slider 84 | [:> ir {:value (get value :time/month) 85 | :step 1 86 | :minValue 1 87 | :maxValue 12 88 | :onChangeComplete #(on-change-internal :time/month % true) 89 | :onChange #(on-change-internal :time/month %)}]]] 90 | [:div.slider-date-time-picker__row 91 | [:div.slider-date-time-picker__row-label "Day"] 92 | [:div.slider-date-time-picker__row-slider 93 | [:> ir {:value (get value :time/date) 94 | :step 1 95 | :minValue 1 96 | :maxValue 31 97 | :onChangeComplete #(on-change-internal :time/date % true) 98 | :onChange #(on-change-internal :time/date %)}]]] 99 | [:div.slider-date-time-picker__row 100 | [:div.slider-date-time-picker__row-label "Hour"] 101 | [:div.slider-date-time-picker__row-slider 102 | [:> ir {:value (get value :time/hour) 103 | :step 1 104 | :minValue 0 105 | :maxValue 23 106 | :onChangeComplete #(on-change-internal :time/hour % true) 107 | :onChange #(on-change-internal :time/hour %)}]]] 108 | [:div.slider-date-time-picker__row 109 | [:div.slider-date-time-picker__row-label "Minute"] 110 | [:div.slider-date-time-picker__row-slider 111 | [:> ir {:value (get value :time/minute) 112 | :step 1 113 | :minValue 0 114 | :maxValue 59 115 | :onChangeComplete #(on-change-internal :time/minute % true) 116 | :onChange #(on-change-internal :time/minute %)}]]]])) 117 | -------------------------------------------------------------------------------- /src/crux_ui/views/query/editor.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query.editor 2 | (:require [crux-ui.views.codemirror :as cm] 3 | [garden.core :as garden] 4 | [re-frame.core :as rf] 5 | [crux-ui.views.query.examples :as query-examples] 6 | [crux-ui.views.style :as s] 7 | [garden.stylesheet :as gs])) 8 | 9 | (def ^:private -sub-query-input (rf/subscribe [:subs.query/input])) 10 | (def ^:private -sub-query-analysis (rf/subscribe [:subs.query/analysis])) 11 | (def ^:private -stats (rf/subscribe [:subs.query/stats])) 12 | (def ^:private -sub-query-input-malformed (rf/subscribe [:subs.query/input-malformed?])) 13 | 14 | (defn- on-qe-change [v] 15 | (rf/dispatch [:evt.ui/query-change v])) 16 | 17 | (def query-ui-styles 18 | [:style 19 | (garden/css 20 | [:.q-editor 21 | {:padding "8px" 22 | :position :relative 23 | :max-height :100% 24 | ;:overflow :scroll 25 | :height :100%} 26 | 27 | [:&__query-type 28 | {:position :absolute 29 | :right :8px 30 | :color s/color-font-secondary 31 | :top :8px 32 | :z-index 10}] 33 | 34 | [:&__error 35 | {:position :absolute 36 | :color :grey 37 | :background :white 38 | :bottom :8px 39 | :z-index 10 40 | :left :24px}]])]) 41 | 42 | 43 | 44 | 45 | (defn root [] 46 | [:div.q-editor 47 | query-ui-styles 48 | [:div.q-editor__query-type (name (:crux.ui/query-type @-sub-query-analysis ""))] 49 | (if-let [e @-sub-query-input-malformed] 50 | [:div.q-editor__error 51 | "Query input appears to be malformed: " (.-message e)]) 52 | 53 | ^{:key @-stats} 54 | [cm/code-mirror 55 | @-sub-query-input 56 | {:on-change on-qe-change 57 | :stats @-stats}]]) 58 | 59 | -------------------------------------------------------------------------------- /src/crux_ui/views/query/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query.examples 2 | (:require [re-frame.core :as rf] 3 | [crux-ui.views.commons.tiny-components :as comps] 4 | [crux-ui.svg-icons :as icon] 5 | [garden.core :as garden] 6 | [garden.stylesheet :as gs] 7 | [reagent.core :as r])) 8 | 9 | 10 | (def ^:private -sub-examples (rf/subscribe [:subs.query/examples])) 11 | 12 | (defn dispatch-examples-close [] 13 | (rf/dispatch [:evt.ui.examples/close])) 14 | 15 | (defn- dispatch-example [ex-title] 16 | (rf/dispatch [:evt.ui.editor/set-example ex-title])) 17 | 18 | (defn is-gist-link? [s] 19 | (let [url (js/URL. s)] 20 | (= "gist.githubusercontent.com" (.-hostname url)))) 21 | 22 | (defn on-examples-add [] 23 | (let [gh-link (js/prompt "Paste a GitHub gist raw content link")] 24 | (if (is-gist-link? gh-link) 25 | (rf/dispatch [:evt.ui/github-examples-request gh-link]) 26 | (js/alert "Please ensure it's a raw gist link")))) 27 | 28 | (def ^:private q-form-styles 29 | [:style 30 | (garden/css 31 | [:.examples 32 | {:display :flex 33 | :width :max-content 34 | :align-items :center 35 | :font-size :13px} 36 | [:&__item 37 | :&__close 38 | {:padding :8px 39 | :cursor :pointer} 40 | [:&:hover 41 | {:color :black}]] 42 | [:&__close 43 | {:line-height 0 44 | :padding "0 8px" 45 | :display :inline-flex 46 | :align-items :center}] 47 | [:&__title 48 | {:padding :8px}] 49 | [:&__import 50 | {:margin-left :8px 51 | :padding :8px}]] 52 | (gs/at-media {:max-width :1000px} 53 | [:.examples 54 | {;:display :none 55 | }]))]) 56 | 57 | (def ^{:private true :const true} examples-close-text 58 | "Tap to close examples. You can always get them back by resetting the cookies on this host.") 59 | 60 | (defn root [] 61 | (if-let [examples @-sub-examples] 62 | [:div.examples 63 | q-form-styles 64 | [:div.examples__close 65 | {:title examples-close-text 66 | :on-click dispatch-examples-close} icon/close] 67 | [:div.examples__title "Examples: "] 68 | (for [{ex-title :title} examples] 69 | ^{:key ex-title} 70 | [:div.examples__item 71 | [comps/button-textual 72 | {:on-click (r/partial dispatch-example ex-title) 73 | :text ex-title}]]) 74 | [:div.examples__import 75 | {:on-click on-examples-add} 76 | "Set my examples"]])) 77 | 78 | -------------------------------------------------------------------------------- /src/crux_ui/views/query/form.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query.form 2 | (:require [re-frame.core :as rf] 3 | [garden.core :as garden] 4 | [garden.stylesheet :as gs] 5 | [crux-ui.views.style :as s] 6 | [crux-ui.views.query.editor :as q-editor] 7 | [crux-ui.views.query.time-controls :as time-controls] 8 | [crux-ui.views.commons.tiny-components :as comps] 9 | [crux-ui.views.query.examples :as query-examples] 10 | [crux-ui.views.functions :as vu])) 11 | 12 | 13 | (def ^:private -sub-editor-key (rf/subscribe [:subs.ui/editor-key])) 14 | (def ^:private -sub-query-analysis (rf/subscribe [:subs.query/analysis])) 15 | 16 | (defn- on-submit [e] 17 | (rf/dispatch [:evt.ui.query/submit {:evt/push-url? true}])) 18 | 19 | 20 | (def ^:private q-form-styles 21 | [:style 22 | (garden/css 23 | [:.examples-wrapper 24 | {:overflow :auto}] 25 | [:.q-form 26 | {:position :relative 27 | :display :grid 28 | :overflow :hidden 29 | :grid-template 30 | "'editor editor' 1fr 31 | 'time-controls submit' auto 32 | / 1fr 128px" 33 | :height :100%} 34 | [:&--examples 35 | {:grid-template 36 | "'editor editor' 1fr 37 | 'time-controls submit' auto 38 | 'examples examples' auto 39 | / 1fr 128px"}] 40 | 41 | [:&__time-controls 42 | {:grid-area "time-controls" 43 | :padding "16px 8px 16px 16px"}] 44 | 45 | [:&__editor 46 | {:grid-area "editor" 47 | :overflow :hidden 48 | :height :100%}] 49 | 50 | [:&__submit 51 | {:grid-area :submit 52 | :text-align :center 53 | :padding "24px 8px" 54 | :place-self "end"} 55 | [:>small 56 | {:font-size :0.8em 57 | :color s/color-font-secondary}]] 58 | 59 | 60 | 61 | [:&__examples 62 | {:grid-area :examples 63 | :overflow :hidden 64 | :width "calc(100% - 32px)" 65 | :border-radius :2px 66 | :background :white 67 | :padding "0px 0px" 68 | :position :relative 69 | :height :47px 70 | :left :16px 71 | :color s/color-font-secondary 72 | :z-index 10} 73 | ["> .examples-wrapper" 74 | {:margin-left "-8px" 75 | :position :absolute 76 | :width :100% 77 | :top 0 78 | :padding-bottom :17px 79 | :overflow :scroll 80 | :bottom "-17px"} 81 | {:overflow-x :scroll 82 | :overflow-y :hidden}] 83 | ["> ::scrollbar" 84 | "> ::-moz-scrollbar" 85 | "> ::-webkit-scrollbar" 86 | {:display :none}]]] 87 | 88 | (gs/at-media {:max-width :1000px} 89 | [:.q-form 90 | {:padding-bottom :32px 91 | :grid-template 92 | "'editor' 1fr 93 | 'time-controls' auto 94 | 'submit' auto 95 | / 100%"} 96 | [:&--examples 97 | {:grid-template 98 | "'editor' 1fr 99 | 'time-controls' auto 100 | 'submit' auto 101 | 'examples' auto 102 | / 100%"}] 103 | [:&__submit 104 | {:padding "0 16px" 105 | :justify-self :start}]]))]) 106 | 107 | (defn root [] 108 | (let [ex (rf/subscribe [:subs.query/examples])] 109 | (fn [] 110 | [:div (vu/bem :q-form (if @ex :examples)) 111 | q-form-styles 112 | [:div.q-form__editor 113 | ^{:key @-sub-editor-key} 114 | [q-editor/root]] 115 | [:div.q-form__time-controls 116 | [time-controls/root {:ui/layout :ui.layout/column}]] 117 | (if @ex 118 | [:div.q-form__examples 119 | [:div.examples-wrapper 120 | [query-examples/root]]]) 121 | [:div.q-form__submit 122 | (let [qa @-sub-query-analysis] 123 | [comps/button-bordered 124 | {:on-click on-submit 125 | :css-mods ["bright" (if-not qa "inactive")] 126 | :text "Run Query"}])]]))) 127 | -------------------------------------------------------------------------------- /src/crux_ui/views/query/time_controls.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query.time-controls 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [crux-ui.views.commons.datepicker-native :as ndt] 5 | [crux-ui.views.query.datepicker-slider :as sdt] 6 | [re-frame.core :as rf] 7 | [reagent.core :as r] 8 | [crux-ui.functions :as f] 9 | [crux-ui.views.functions :as vu])) 10 | 11 | (def -sub-time (rf/subscribe [:subs.query/time])) 12 | (def -sub-vt (rf/subscribe [:subs.query.time/vt])) 13 | (def -sub-tt (rf/subscribe [:subs.query.time/tt])) 14 | (def -sub-vtc (rf/subscribe [:subs.query.time/vtc])) 15 | (def -sub-ttc (rf/subscribe [:subs.query.time/ttc])) 16 | 17 | (defn on-time-commit [^keyword time-type ^js/Date time] 18 | (rf/dispatch [:evt.ui.query/time-commit time-type time])) 19 | 20 | (def on-time-commit-debounced 21 | (f/debounce on-time-commit 1000)) 22 | 23 | 24 | (defn on-vt-change [d] 25 | (println :on-vt-change d) 26 | #_(on-time-commit-debounced :time/vt d) 27 | (rf/dispatch [:evt.ui.query/time-change :time/vt d])) 28 | 29 | (defn on-vt-commit [d] 30 | (println :on-vt-commit d) 31 | (on-time-commit :time/vt d)) 32 | 33 | (defn on-tt-commit [d] 34 | (on-time-commit :time/tt d)) 35 | 36 | (defn on-tt-change [d] 37 | #_(on-time-commit-debounced :time/tt d) 38 | (rf/dispatch [:evt.ui.query/time-change :time/tt d])) 39 | 40 | 41 | (def ^:private time-controls-styles 42 | [:style 43 | (garden/css 44 | [:.time-controls 45 | {:display :flex 46 | :flex-direction :column 47 | :justify-content :space-between} 48 | [:&__item 49 | {:margin "8px 0"}] 50 | [:&__switcher 51 | {:margin-top :16px}]] 52 | (gs/at-media {:min-width :1000px} 53 | [:.time-controls--column 54 | {:flex-direction :row}]))]) 55 | 56 | (def ^:private -sub-bp-375 (rf/subscribe [:subs.ui.responsive-breakpoints/width-lt-375])) 57 | 58 | (defn native-pickers [layout] 59 | [:<> 60 | [:div.time-controls__item 61 | [ndt/picker 62 | {:label "Valid time" 63 | :ui/layout layout 64 | :value @-sub-vt 65 | :on-change on-vt-change}]] 66 | [:div.time-controls__item 67 | [ndt/picker 68 | {:label (if @-sub-bp-375 "Tx time" "Transaction time") 69 | :ui/layout layout 70 | :value @-sub-tt 71 | :on-change on-tt-change}]]]) 72 | 73 | 74 | (defn range-pickers [] 75 | (let [active-picker (r/atom ::pickers-vt) 76 | toggle {::pickers-vt ::pickers-tt 77 | ::pickers-tt ::pickers-vt} 78 | toggle-time #(swap! active-picker toggle)] 79 | (fn [] 80 | (if (= ::pickers-vt @active-picker) 81 | [:<> 82 | [sdt/root-simple 83 | {:label "Valid time" 84 | :value @-sub-vtc 85 | :on-change-complete on-vt-commit 86 | :on-change on-vt-change}] 87 | [:label.time-controls__switcher 88 | {:on-click toggle-time} "Tx time: " (str @-sub-tt)]] 89 | [:<> 90 | [sdt/root-simple 91 | {:label "Tx time" 92 | :value @-sub-ttc 93 | :on-change-complete on-tt-commit 94 | :on-change on-tt-change}] 95 | [:label.time-controls__switcher 96 | {:on-click toggle-time} "Valid time: " (str @-sub-vt)]])))) 97 | 98 | 99 | (defn root [{:ui/keys [layout]}] 100 | [:div {:class (vu/bem-str :time-controls layout)} 101 | 102 | #_sdt/style 103 | ndt/style 104 | time-controls-styles 105 | [native-pickers layout] 106 | #_[range-pickers]]) 107 | 108 | -------------------------------------------------------------------------------- /src/crux_ui/views/query_perf.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query-perf 2 | (:require [garden.core :as garden] 3 | [crux-ui.views.charts.query-perf-plots :as perf-plots] 4 | [cljs.reader :as edn])) 5 | 6 | 7 | (def plots-data 8 | (if-let [script (js/document.getElementById "plots-data")] 9 | (edn/read-string (.-textContent script)) 10 | (println :plots-data-not-found))) 11 | 12 | (def q1-data 13 | {:title "Simple query execution time" 14 | :plain (:q1 plots-data) 15 | :with-cache (:q1-with-cache plots-data)}) 16 | 17 | (def q3-data 18 | {:title "Query with joins execution time" 19 | :plain (:q3 plots-data) 20 | :with-cache (:q3-with-cache plots-data)}) 21 | 22 | 23 | (def ^:private root-styles 24 | [:style 25 | (garden/css 26 | [[:a]])]) 27 | 28 | (defn root [] 29 | [:div.query-perf 30 | [:div.query-perf__plot1 31 | [perf-plots/root q1-data]] 32 | [:div.query-perf__plot1 33 | [perf-plots/root q3-data]]]) 34 | -------------------------------------------------------------------------------- /src/crux_ui/views/query_ui.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.query-ui 2 | (:require [re-frame.core :as rf] 3 | [garden.core :as garden] 4 | [garden.stylesheet :as gs] 5 | [crux-ui.views.query.form :as q-form] 6 | [crux-ui.views.query.output :as q-output] 7 | [crux-ui.views.style :as s] 8 | [crux-ui.views.functions :as vu])) 9 | 10 | 11 | (def ^:private query-ui-styles 12 | [:style 13 | (garden/css 14 | [:.query-ui 15 | {:font-size :16px 16 | :border-radius :2px 17 | :margin "0 1rem 8px" 18 | :border s/q-ui-border 19 | :width :100% 20 | :overflow :hidden 21 | :height "calc(100% - 8px)" 22 | :display :grid 23 | :place-items :stretch 24 | :grid-template 25 | "'form output' 100% / minmax(512px, 38%) 62%"} 26 | [:&--query 27 | {:grid-template 28 | "'form output' 100% / 1fr 0px"} 29 | [">.query-ui__output" 30 | {:display :none}]] 31 | 32 | [:&--output 33 | {:grid-template 34 | "'form output' 100% / 0 100%"} 35 | [">.query-ui__form" 36 | {:display :none}]] 37 | 38 | [:&--horizontal 39 | {:grid-template 40 | "'output' calc(100% - 330px) 41 | 'form' 330px"}] 42 | 43 | [:&__form 44 | {:padding "0px 0" 45 | :overflow :hidden 46 | :grid-area :form}] 47 | 48 | [:&--form-minimised 49 | {:grid-template 50 | "'form output' 100% / minmax(0px, 0%) 100%"}] 51 | 52 | [:&__output 53 | {:padding "0px 0" 54 | :grid-area :output 55 | :border-left s/q-ui-border}]] 56 | 57 | (gs/at-media {:max-width :1000px} 58 | [:.query-ui 59 | {:margin 0}]))]) 60 | 61 | (def ^:private mode->class-mod 62 | {:ui.display-mode/query "query" 63 | :ui.display-mode/output "output"}) 64 | 65 | (def -sub-display-mode (rf/subscribe [:subs.ui/display-mode])) 66 | (def -sub-show-form (rf/subscribe [:subs.db.ui/show-form?])) 67 | 68 | (defn query-ui [] 69 | [:div 70 | (vu/bem :query-ui 71 | (if-not @-sub-show-form :form-minimised) 72 | (mode->class-mod @-sub-display-mode)) 73 | query-ui-styles 74 | [:div.query-ui__form 75 | [q-form/root]] 76 | [:div.query-ui__output 77 | [q-output/root]]]) 78 | -------------------------------------------------------------------------------- /src/crux_ui/views/second_layer.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.second-layer 2 | (:require [crux-ui.views.settings :as settings] 3 | [crux-ui.views.sidebar :as sidebar] 4 | [crux-ui.views.overview :as overview] 5 | [garden.core :as garden] 6 | [garden.stylesheet :as gs] 7 | [re-frame.core :as rf])) 8 | 9 | (def ^:private main-pane-views 10 | {:db.ui.second-layer.main-pane/overview overview/root 11 | :db.ui.second-layer.main-pane/settings settings/root}) 12 | 13 | (def ^:private root-styles 14 | [:style 15 | (garden/css 16 | [:.second-layer 17 | {:width :100% 18 | :height :100% 19 | :display :grid 20 | :grid-gap :16px 21 | :grid-template "'side main' 1fr / 264px 1fr"} 22 | [:&__side 23 | {:grid-area :side 24 | :overflow :scroll}] 25 | [:&__main 26 | {:grid-area :main 27 | :background :white 28 | :border-radius :2px 29 | :height :100% 30 | :overflow :scroll}]] 31 | (gs/at-media {:max-width :1000px} 32 | [:.second-layer 33 | {:grid-template "'main' 1fr / 1fr"} 34 | [:&__side 35 | {:grid-area :main}] 36 | [:&__main 37 | {:grid-area :main}]]))]) 38 | 39 | (defn autoclose-on-fader-click [evt] 40 | (let [target (.-target evt)] 41 | (if (= "second-layer" (.-id target)) 42 | (rf/dispatch [:evt.ui.second-layer/toggle])))) 43 | 44 | (defn root [] 45 | (let [-sub-second-layer-main-pane (rf/subscribe [:subs.db.ui.second-layer/main-pane])] 46 | (fn [] 47 | [:div#second-layer.second-layer {:on-click autoclose-on-fader-click} 48 | root-styles 49 | [:div.second-layer__side [sidebar/root]] 50 | (let [mpv (main-pane-views @-sub-second-layer-main-pane)] 51 | (if mpv 52 | [:div.second-layer__main [mpv]]))]))) 53 | 54 | -------------------------------------------------------------------------------- /src/crux_ui/views/settings.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.settings 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [reagent.core :as r] 5 | [re-frame.core :as rf] 6 | [crux-ui.views.commons.input :as input] 7 | [crux-ui.views.commons.form-line :as fl] 8 | [crux-ui.subs] 9 | [crux-ui.functions :as f] 10 | [crux-ui.views.output.edn :as output-edn] 11 | [crux-ui.views.commons.tiny-components :as comps] 12 | [crux-ui.views.commons.dom :as dom])) 13 | 14 | 15 | (def ^:private root-styles 16 | [:style 17 | (garden/css 18 | [:.g-spacer-w-40 19 | {:height :16px 20 | :display :inline-block 21 | :width :40px}] 22 | [:.g-container 23 | {:height :100% 24 | :overflow :hidden} 25 | [:&__content 26 | {:overflow :auto 27 | :height :100%}]] 28 | [:.g-sticky 29 | {:position :sticky 30 | :top :0px}] 31 | [:.line 32 | {} 33 | [:&__label 34 | {:margin-bottom :4px}] 35 | [:&__control 36 | {:max-width :300px}]] 37 | [:.settings 38 | {:padding "0px 32px 32px"} 39 | [:&__title 40 | {:padding-top :16px 41 | :background :white}] 42 | [:&__line 43 | {:margin-top "40px"} 44 | [:&--submit 45 | {}]] 46 | [:&__status 47 | {:margin-top "24px"} 48 | [:>h3 49 | {:font-weight 400 50 | :font-size :20px 51 | :margin-bottom :8px}]]])]) 52 | 53 | (defn- on-prop-change [prop-name {v :value :as change-complete-evt}] 54 | (rf/dispatch 55 | [:evt.db/prop-change 56 | {:evt/prop-name prop-name 57 | :evt/value v}])) 58 | 59 | (defn- on-host-change [{v :value :as change-complete-evt}] 60 | (rf/dispatch [:evt.db/host-change v])) 61 | 62 | (defn- on-commit [arg] 63 | (println "changes commit" (pr-str arg)) 64 | (rf/dispatch [:evt.db/props-change arg])) 65 | 66 | (defn close [] 67 | (rf/dispatch [:evt.ui.second-layer.main-pane/cancel])) 68 | 69 | (defn root [] 70 | (let [-sub-settings (rf/subscribe [:subs.sys/settings]) 71 | -sub-host-status (rf/subscribe [:subs.sys.host/status]) 72 | -local-atom (r/atom @-sub-settings) 73 | commit-changes #(on-commit (dissoc @-local-atom :db.sys.host/status)) 74 | 75 | on-prop-change 76 | (fn on-prop-change [prop-name {v :value :as change-complete-evt}] 77 | (swap! -local-atom assoc prop-name v)) 78 | 79 | on-host-change-internal 80 | (fn [{v :value :as evt}] 81 | (on-host-change evt) 82 | (on-prop-change :db.sys/host evt))] 83 | 84 | (fn [] 85 | [:div.settings 86 | root-styles 87 | [:h1.settings__title.g-sticky "Settings"] 88 | 89 | [:div.settings__line 90 | [fl/line 91 | {:label "Attribute history data points limit per entity" 92 | :control 93 | [input/text :ui.settings/attr-qlimit 94 | {:on-change-complete (r/partial on-prop-change :db.query.attr-history/docs-limit) 95 | :parse-fn dom/parse-int-or-nil 96 | :value (:db.query.attr-history/docs-limit @-local-atom)}]}]] 97 | 98 | [:div.settings__line 99 | [fl/line 100 | {:label "Query results limit" 101 | :control 102 | [input/text :ui.settings/qlimit 103 | {:on-change-complete (r/partial on-prop-change :db.query/limit) 104 | :parse-fn dom/parse-int-or-nil 105 | :value (:db.query/limit @-local-atom)}]}]] 106 | 107 | [:div.settings__line 108 | [fl/line 109 | {:label "Crux HTTP-Server Host and port" 110 | :control 111 | [input/text :ui.settings/host 112 | {:on-change-complete on-host-change-internal 113 | :value (:db.sys/host @-local-atom)}]}] 114 | ^{:key @-sub-host-status} 115 | [:pre.settings__status 116 | (let [s @-sub-host-status] 117 | (if s 118 | [:<> 119 | [:h3 "connection successful"] 120 | [output-edn/simple-print s]] 121 | [:h3 "connection failed"]))]] 122 | 123 | [:div.settings__line.settings__line--submit 124 | [comps/button-cta 125 | {:text "Apply" 126 | :on-click commit-changes}] 127 | [:div.g-spacer-w-40] 128 | [comps/button-textual 129 | {:text "Cancel" 130 | :on-click close}]]]))) 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/crux_ui/views/sidebar.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.sidebar 2 | (:require [garden.core :as garden] 3 | [garden.stylesheet :as gs] 4 | [clojure.string :as s] 5 | [crux-ui.views.commons.cube-svg :as cube] 6 | [crux-ui.views.functions :as vf] 7 | [crux-ui.routes :as routes] 8 | [crux-ui.views.commons.css-logo :as css-logo] 9 | [re-frame.core :as rf] 10 | [crux-ui.views.commons.tiny-components :as comps] 11 | [crux-ui.config :as cfg] 12 | [reagent.core :as r])) 13 | 14 | 15 | (def ^:private sidebar-styles 16 | [:style 17 | (garden/css 18 | [:.sidebar 19 | {:width :100% 20 | :min-height :100% 21 | :background :white 22 | :border-radius :2px 23 | :padding "0px 0px 48px"} 24 | [:&__item 25 | {:cursor :pointer 26 | :padding "16px 24px"} 27 | [:&:hover 28 | {:background "hsla(0, 0%, 0%, 0.1)"}] 29 | [:&--logo 30 | {:padding "12px 16px"}]]] 31 | (gs/at-media {:max-width :500px} 32 | [:.css-logo__text 33 | {:display :none}]) 34 | (gs/at-media {:max-width :375px} 35 | [:.css-logo__cube 36 | {:width :40px 37 | :height :40px 38 | :flex "0 0 40px"}]))]) 39 | 40 | 41 | (defn -item [{:keys [dispatch] :as attrs} contents] 42 | [:div.sidebar__item 43 | (cond-> attrs 44 | dispatch (-> (assoc :on-click (r/partial rf/dispatch dispatch)) 45 | (dissoc :dispatch))) 46 | contents]) 47 | 48 | 49 | (defn root [] 50 | (let [-sub-examples (rf/subscribe [:subs.query/examples]) 51 | -sub-qmap? (rf/subscribe [:subs.query/is-query-map?])] 52 | (fn [] 53 | [:div.sidebar 54 | sidebar-styles 55 | [-item {:class "sidebar__item--logo" 56 | :dispatch [:evt.ui.second-layer/toggle]} [css-logo/root]] 57 | [-item {:dispatch [:evt.ui/fullscreen]} "Fullscreen mode"] 58 | (if @-sub-qmap? 59 | [-item {:dispatch [:evt.ui/toggle-polling]} "Toggle polling"]) 60 | [-item {:dispatch [:evt.ui.sidebar/show-settings]} "Settings"] 61 | [-item {:dispatch [:evt.ui.sidebar/show-overview]} "Console Overview"] 62 | (if-not @-sub-examples 63 | [-item {:dispatch [:evt.ui/restore-examples]} "Restore Examples"]) 64 | [-item {:dispatch [:evt.ui/import-examples]} "Import Examples"] 65 | [-item {} [comps/link-outer cfg/url-docs "Crux Docs"]] 66 | [-item {} [comps/link-outer cfg/url-chat "Crux Chat"]] 67 | [-item {} [comps/link-mailto cfg/url-mail]]]))) 68 | 69 | (comment 70 | (routes/path-for :rd/query-ui) 71 | (routes/path-for :rd/settings)) 72 | -------------------------------------------------------------------------------- /src/crux_ui/views/style.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui.views.style) 2 | 3 | (def color-placeholder "grey") 4 | (def color-font-secondary "hsl(0,0%,55%)") 5 | (def q-ui-border "1px solid hsl(0,0%,85%)") 6 | 7 | (defn hsl 8 | ([{:keys [h s l a] :as conf}] 9 | (if a 10 | (str "hsla(" h "," s "%," l "%," a ")") 11 | (str "hsl(" h "," s "%," l "%)"))) 12 | ([h s l] 13 | (str "hsl(" h "," s "%," l "%)")) 14 | ([h s l a] 15 | (str "hsl(" h "," s "%," l "%," a ")"))) 16 | -------------------------------------------------------------------------------- /src/crux_ui_lib/functions.cljc: -------------------------------------------------------------------------------- 1 | (ns crux-ui-lib.functions 2 | #?(:cljs (:require [cljs.reader :as edn])) 3 | #?(:clj (:require [clojure.tools.reader.edn :as edn]))) 4 | 5 | (defn- single-int-or-bool? [v] 6 | (let [v1 (first v)] 7 | (and (= 1 (count v)) 8 | (or (nat-int? v1) 9 | (boolean? v1))))) 10 | 11 | (defn- normalize-query-part [v] 12 | "this strips out limit/offset/full-results into singular variable" 13 | (if (single-int-or-bool? v) 14 | (first v) 15 | (vec v))) 16 | 17 | ; todo critical review target 18 | 19 | (defn normalize-query [q] 20 | (cond 21 | (vector? q) (into {} (for [[[k] v] (->> (partition-by keyword? q) 22 | (partition-all 2))] 23 | [k (normalize-query-part v)])) 24 | (string? q) (if-let [q (try 25 | (edn/read-string q) 26 | #?(:cljs (catch :default e e) 27 | :clj (catch Exception e)))] 28 | (normalize-query q) 29 | q) 30 | :else 31 | q)) 32 | ; adapted from https://github.com/juxt/crux/blob/3368595d7fcaec726b1a602a9ec75e325b49ecd6/src/crux/query.clj#L1013 33 | -------------------------------------------------------------------------------- /src/crux_ui_lib/http_functions.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-ui-lib.http-functions 2 | (:require [promesa.core :as p :include-macros true] 3 | [clojure.edn :as edn])) 4 | 5 | (defmulti fetch type) 6 | 7 | (defmethod fetch :default [{:keys [method url] :as opts}] 8 | (if method (assert (#{:post :get} method) (str "Unsupported HTTP method: " (:method opts)))) 9 | (let [opts (-> opts 10 | (update :method (fnil name :get)) 11 | (assoc :referrerPolicy "origin"))] 12 | (p/alet [fp (js/fetch url (clj->js opts)) 13 | resp (p/await fp) 14 | headers (.-headers resp) 15 | content-type (.get headers "Content-Type") 16 | text (p/await (.text resp))] 17 | {:body text 18 | :status (.-status resp) 19 | :headers {:content-type content-type}}))) 20 | 21 | (defmethod fetch js/String [url] 22 | (fetch {:url url :method :get})) 23 | 24 | (defn fetch-edn [prms] 25 | (p/map #(update % :body edn/read-string) (fetch prms))) 26 | 27 | -------------------------------------------------------------------------------- /src/crux_ui_server/config.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.config 2 | (:require [clojure.java.io :as io] 3 | [clojure.edn :as edn]) 4 | (:import (java.io FileNotFoundException PushbackReader IOException File))) 5 | 6 | (def ^:private defaults 7 | {:console/frontend-port 5000 8 | :console/embed-crux false 9 | :console/routes-prefix "/console" 10 | :console/crux-node-url-base "localhost:8080/crux" 11 | :console/hide-features #{:features/attribute-history} 12 | :console/crux-http-port 8080}) 13 | 14 | (defn load-edn 15 | "Load edn from an io/reader source (filename or io/resource)." 16 | [source] 17 | (try 18 | (with-open [r (io/reader source)] 19 | (edn/read (PushbackReader. r))) 20 | (catch IOException e 21 | (printf "Couldn't open '%s': %s\n" source (.getMessage e))) 22 | (catch RuntimeException e 23 | (printf "Error parsing edn file '%s': %s\n" source (.getMessage e))))) 24 | 25 | (def ^:private default-conf-name "crux-console-conf.edn") 26 | 27 | (defn- try-load-conf [^String user-conf-filename] 28 | (if (and user-conf-filename 29 | (not (.exists (io/as-file user-conf-filename)))) 30 | (throw (FileNotFoundException. (str "file " user-conf-filename " not found")))) 31 | (let [conf-file-name (or user-conf-filename default-conf-name) 32 | file ^File (io/as-file conf-file-name)] 33 | (when (.exists file) 34 | (println "loading conf from " user-conf-filename) 35 | (load-edn file)))) 36 | 37 | (defn- fix-key [[k v]] 38 | [(cond-> k 39 | (and (string? k) (.startsWith k "--")) 40 | (subs 2)) 41 | v]) 42 | 43 | (defn- parse-args [args-map] 44 | (let [w-norm-keys (into {} (map fix-key args-map)) 45 | w-kw-keys (into {} (map (fn [[k v]] [(keyword "console" k) v]) w-norm-keys)) 46 | w-parsed (into {} (map (fn [[k v]] [k (cond-> v (string? v) read-string)]) w-kw-keys))] 47 | w-parsed)) 48 | 49 | (assert 50 | (= {:console/frontend-port 5000, 51 | :console/embed-crux false, 52 | :console/crux-http-port 8080} 53 | (parse-args 54 | {"--frontend-port" "5000" 55 | "--embed-crux" "false" 56 | "--crux-http-port" "8080"}))) 57 | 58 | (defn calc-conf [args] 59 | (let [conf-filename (get args "--conf-file") 60 | args (dissoc args "--conf-file")] 61 | (merge defaults 62 | (try-load-conf conf-filename) 63 | (parse-args args)))) 64 | 65 | -------------------------------------------------------------------------------- /src/crux_ui_server/crux_auto_start.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.crux-auto-start) 2 | 3 | (def ^:private node-opts 4 | {:crux.node/topology :crux.standalone/topology 5 | :crux.standalone/event-log-dir "data/eventlog-1" 6 | :crux.kv/kv-backend :crux.kv.rocksdb/kv 7 | :crux.kv/db-dir "data/db-dir-1"}) 8 | 9 | (def ^:private http-opts 10 | {:server-port 8080 11 | :cors-access-control 12 | [:access-control-allow-origin [#".*"] 13 | :access-control-allow-headers 14 | ["X-Requested-With" "X-Custom-Header" "Content-Type" "Cache-Control" 15 | "Origin" "Accept" "Authorization"] 16 | :access-control-allow-methods [:get :options :head :post]]}) 17 | 18 | (defn try-start-servers [{:keys [console/embed-crux console/crux-http-port]}] 19 | (try 20 | (let [start-node (requiring-resolve 'crux.api/start-node) 21 | start-http-server (requiring-resolve 'crux.http-server/start-http-server) 22 | node (start-node node-opts) 23 | http-opts (assoc http-opts :server-port crux-http-port) 24 | crux-http-server (start-http-server node http-opts)] 25 | {:crux-node node 26 | :crux-http-server crux-http-server}) 27 | (catch ClassNotFoundException e 28 | (println "failed to load crux.http-server or crux.api, are they on the classpath?") 29 | {}))) 30 | -------------------------------------------------------------------------------- /src/crux_ui_server/generate_web_manifest.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.generate-web-manifest) 2 | 3 | (defn generate-str [{:console/keys [routes-prefix] :as conf}] 4 | (str "{ 5 | \"name\" : \"Crux Console\", 6 | \"short_name\" : \"Crux Console\", 7 | \"start_url\" : \"" routes-prefix "/app\", 8 | \"display\" : \"standalone\", 9 | \"theme_color\" : \"hsl(32, 91%, 54%)\", 10 | \"background_color\" : \"rgb(167, 191, 232)\", 11 | \"description\" : \"Console for Crux to see data in perspectives\", 12 | \"icons\" : [{ 13 | \"src\" : \"" routes-prefix "/static/img/cube-on-white-120.png\", 14 | \"sizes\" : \"120x120\", 15 | \"type\" : \"image/png\" 16 | }, { 17 | \"src\" : \"" routes-prefix "/static/img/cube-on-white-192.png\", 18 | \"sizes\" : \"192x192\", 19 | \"type\" : \"image/png\" 20 | }, { 21 | \"src\" : \"" routes-prefix "/static/img/cube-on-white-512.png\", 22 | \"sizes\" : \"512x512\", 23 | \"type\" : \"image/png\" 24 | }] 25 | }")) 26 | -------------------------------------------------------------------------------- /src/crux_ui_server/main.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.main 2 | "Routing and bootstrapping" 3 | (:require [aleph.http :as http] 4 | [bidi.bidi :as bidi] 5 | [crux-ui-server.crux-auto-start :as crux-auto-start] 6 | [crux-ui-server.config :as config] 7 | [crux-ui-server.generate-web-manifest :as manifest] 8 | [clojure.tools.logging :as log] 9 | [crux-ui-server.pages :as pages] 10 | [clojure.java.io :as io] 11 | [clojure.string :as s] 12 | [clojure.pprint :as pprint]) 13 | (:gen-class) 14 | (:import (java.io Closeable))) 15 | 16 | (defonce closables (atom {})) 17 | 18 | (defonce config (atom nil)) 19 | (defonce routes (atom nil)) 20 | 21 | (defn calc-routes [routes-prefix] 22 | [routes-prefix 23 | [["" ::home] 24 | ["/" ::home] 25 | ["/app" 26 | {"" ::console 27 | "/output" ::console 28 | ["/output/" :rd/out-tab] ::console 29 | ["/" :rd/tab] ::console}] 30 | ["/query-perf" ::query-perf] 31 | ["/service-worker-for-console.js" ::service-worker-for-console] 32 | ["/manifest.json" ::web-app-manifest] 33 | ["/static/" 34 | {true ::static}] 35 | [true ::not-found]]]) 36 | 37 | 38 | (comment 39 | (bidi/match-route @routes "/console") 40 | (bidi/match-route @routes "/console/app") 41 | (reset! routes (calc-routes "/console"))) 42 | 43 | 44 | (defmulti handler 45 | (fn [{:keys [uri] :as req}] 46 | (println ::uri uri) 47 | (some-> (bidi/match-route @routes uri) :handler))) 48 | 49 | (defmethod handler ::home [req] 50 | {:status 301 51 | :headers {"location" (bidi/path-for @routes ::console)}}) 52 | 53 | (defmethod handler ::console [req] 54 | {:status 200 55 | :headers {"content-type" "text/html"} 56 | :body (pages/gen-console-page req @config)}) 57 | 58 | (defmethod handler ::service-worker-for-console [req] 59 | {:status 200 60 | :headers {"content-type" "text/javascript"} 61 | :body (pages/gen-service-worker req @config)}) 62 | 63 | (defmethod handler ::web-app-manifest [req] 64 | {:status 200 65 | :headers {"content-type" "application/json"} 66 | :body (manifest/generate-str @config)}) 67 | 68 | (defn uri->mime-type [uri] 69 | (cond 70 | (re-find #".css$" uri) "text/css" 71 | (re-find #".js$" uri) "text/javascript" 72 | (re-find #".json$" uri) "application/json" 73 | (re-find #".png$" uri) "image/png" 74 | (re-find #".jpe?g$" uri) "image/jpeg" 75 | (re-find #".svg$" uri) "image/svg" 76 | :else "text/plain")) 77 | 78 | (defmethod handler ::static [{:keys [uri] :as req}] 79 | (let [relative-uri (s/replace uri (re-pattern (str "^" @pages/routes-prefix)) "") 80 | relative-uri (s/replace relative-uri #"^/" "") 81 | resource (io/resource relative-uri)] 82 | (if resource 83 | (let [stream (io/input-stream resource) 84 | mime-type (uri->mime-type uri)] 85 | {:status 200 86 | :headers {"content-type" mime-type} 87 | :body stream}) 88 | (log/warn "No resource for" uri)))) 89 | 90 | (defmethod handler ::not-found [req] 91 | {:status 200 92 | :headers {"content-type" "text/plain"} 93 | :body "Not found"}) 94 | 95 | (defmethod handler :default [req] 96 | {:status 200 97 | :headers {"content-type" "text/plain"} 98 | :body "Default handler : Not implemented"}) 99 | 100 | (defn stop-servers [] 101 | (when-let [closables' (not-empty @closables)] 102 | (println "stopping console server") 103 | (doseq [^Closeable closable (vals closables')] 104 | (.close closable)) 105 | (reset! closables {}))) 106 | 107 | (defn- start-servers [{:keys [console/frontend-port console/embed-crux] :as conf}] 108 | (swap! closables assoc :frontend (http/start-server handler {:port frontend-port})) 109 | (if embed-crux 110 | (swap! closables merge (crux-auto-start/try-start-servers conf)))) 111 | 112 | 113 | (defn -main 114 | "Accepted args 115 | --frontend-port 5000 116 | --conf-file crux-console-conf.edn 117 | --embed-crux false 118 | --crux-http-server-port 8080" 119 | [& {:as args}] 120 | (.addShutdownHook (Runtime/getRuntime) (Thread. #'stop-servers)) 121 | (let [conf (config/calc-conf args)] 122 | (reset! routes (calc-routes (:console/routes-prefix conf))) 123 | (reset! config conf) 124 | (reset! pages/routes-prefix (:console/routes-prefix conf)) 125 | (println 126 | (str "starting console server w conf: \n" 127 | (with-out-str (pprint/pprint conf)))) 128 | (start-servers conf))) 129 | -------------------------------------------------------------------------------- /src/crux_ui_server/pages.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.pages 2 | (:require [crux-ui-server.preloader :as preloader] 3 | [page-renderer.api :as pr] 4 | [clojure.java.io :as io]) 5 | (:import (clojure.lang IPersistentMap))) 6 | 7 | (def routes-prefix (atom "")) 8 | 9 | (defn console-assets-frame ^IPersistentMap 10 | [^IPersistentMap config] 11 | (let [routes-prefix (:console/routes-prefix config)] 12 | {:title "Crux Console" 13 | :lang "en" 14 | :theme-color "hsl(32, 91%, 54%)" 15 | :og-image (str routes-prefix "/static/img/crux-logo.svg") 16 | :link-apple-icon (str routes-prefix "/static/img/cube-on-white-192.png") 17 | :link-apple-startup-image (str routes-prefix "/static/img/cube-on-white-512.png") 18 | :link-image-src (str routes-prefix "/static/img/cube-on-white-512.png") 19 | :service-worker (str routes-prefix "/service-worker-for-console.js") 20 | :favicon (str routes-prefix "/static/img/cube-on-white-120.png") 21 | 22 | :doc-attrs 23 | {:data-routes-prefix routes-prefix 24 | :data-hide-features (pr-str (:console/hide-features config)) 25 | :data-crux-base-url (:console/crux-node-url-base config) 26 | :data-crux-http-port (str (:console/crux-http-port config))} 27 | 28 | :sw-default-url (str routes-prefix "/app") 29 | :stylesheet-async 30 | [(str routes-prefix "/static/styles/reset.css") 31 | (str routes-prefix "/static/styles/react-input-range.css") 32 | (str routes-prefix "/static/styles/react-ui-tree.css") 33 | (str routes-prefix "/static/styles/codemirror.css") 34 | (str routes-prefix "/static/styles/monokai.css") 35 | (str routes-prefix "/static/styles/eclipse.css")] 36 | :script (str routes-prefix "/static/crux-ui/compiled/main.js") 37 | :manifest (str routes-prefix "/manifest.json") 38 | :head-tags 39 | [[:style#_stylefy-constant-styles_] 40 | [:style#_stylefy-styles_] 41 | [:meta {:name "google" :content "notranslate"}]] 42 | :body [:body [:div#app preloader/root]]})) 43 | 44 | (defn gen-console-page [req config] 45 | (pr/render-page (console-assets-frame config))) 46 | 47 | (defn gen-service-worker [req config] 48 | (pr/generate-service-worker (console-assets-frame config))) 49 | 50 | (defn q-perf-page [req] 51 | (pr/render-page 52 | {:title "Crux Console" 53 | :lang "en" 54 | :doc-attrs {:data-scenario :perf-plot} 55 | :stylesheet-async (str @routes-prefix "/static/styles/reset.css") 56 | :og-image (str @routes-prefix "/static/img/crux-logo.svg") 57 | :script (str @routes-prefix "/static/crux-ui/compiled/main-perf.js") 58 | :head-tags [[:script {:id "plots-data" :type "text/edn"} 59 | (slurp (io/resource "static/plots-data.edn"))]] 60 | :body [:body [:div#app preloader/root]]})) 61 | 62 | (defn gen-home-page [req] 63 | (pr/render-page 64 | {:title "Crux Standalone Demo with HTTP" 65 | :lang "en" 66 | :og-image (str @routes-prefix "/static/img/crux-logo.svg") 67 | :body 68 | [:body 69 | [:header 70 | [:div.nav 71 | [:div.logo {:style {:opacity "0"}} 72 | [:a {:href (str @routes-prefix "/")} 73 | [:img.logo-img {:src (str @routes-prefix "/static/img/crux-logo.svg")}]]] 74 | [:div.n0 75 | [:a {:href "https://juxt.pro/crux/docs/index.html"} [:span.n "Documentation"]] 76 | [:a {:href "https://juxt-oss.zulipchat.com/#narrow/stream/194466-crux"} [:span.n "Community Chat"]] 77 | [:a {:href "mailto:crux@juxt.pro"} [:span.n "crux@juxt.pro"]]]]] 78 | [:div {:style {:text-align "center" :width "100%" :margin-top "6em"}} 79 | [:div.splash {:style {:max-width "25vw" :margin-left "auto" :margin-right "auto"}} 80 | [:a {:href (str @routes-prefix "/")} 81 | [:img.splash-img {:src (str @routes-prefix "/static/img/crux-logo.svg")}]]] 82 | 83 | [:div {:style {:height "4em"}}] 84 | [:h3 "You should now be able to access this Crux standalone demo node using the HTTP API via localhost:8080"]] 85 | 86 | [:div#app]]})) 87 | -------------------------------------------------------------------------------- /src/crux_ui_server/preloader.clj: -------------------------------------------------------------------------------- 1 | (ns crux-ui-server.preloader) 2 | 3 | (def ^:private style 4 | [:style 5 | "html, body, #app, .preloader { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .preloader { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | .scene { 17 | width: 200px; 18 | height: 200px; 19 | perspective: 600px; 20 | } 21 | 22 | .cube { 23 | width: 100%; 24 | height: 100%; 25 | position: relative; 26 | transform-style: preserve-3d; 27 | transform: translateZ(-100px) rotateX(-45deg) rotateY(-45deg); 28 | } 29 | 30 | .cube__face { 31 | position: absolute; 32 | width: 200px; 33 | height: 200px; 34 | border: 1px solid orange; 35 | } 36 | 37 | .cube__face--front { transform: rotateY( 0deg); } 38 | .cube__face--right { transform: rotateY( 90deg); } 39 | .cube__face--back { transform: rotateY(180deg); } 40 | .cube__face--left { transform: rotateY(-90deg); } 41 | .cube__face--top { transform: rotateX( 90deg); } 42 | .cube__face--bottom { transform: rotateX(-90deg); } 43 | 44 | 45 | .cube__face--front { transform: rotateY( 0deg) translateZ(100px); } 46 | .cube__face--right { transform: rotateY( 90deg) translateZ(100px); } 47 | .cube__face--back { transform: rotateY(180deg) translateZ(100px); } 48 | .cube__face--left { transform: rotateY(-90deg) translateZ(100px); } 49 | .cube__face--top { transform: rotateX( 90deg) translateZ(100px); } 50 | .cube__face--bottom { transform: rotateX(-90deg) translateZ(100px); } 51 | 52 | .cube__face.cube__face--back {} 53 | 54 | .cube__face { 55 | border-color: gray !important; 56 | } 57 | 58 | 59 | @keyframes pulse-bottom-left { 60 | 0% { 61 | border-bottom: 1px solid gray; 62 | border-left: 1px solid gray; 63 | } 64 | 25% { 65 | border-bottom: 1px solid orange; 66 | border-left: 1px solid orange; 67 | } 68 | 50% { 69 | border-left: 1px solid gray; 70 | border-top: 1px solid gray; 71 | } 72 | } 73 | @keyframes pulse-bottom-right { 74 | 0% { 75 | border-bottom: 1px solid gray; 76 | border-right: 1px solid gray; 77 | } 78 | 25% { 79 | border-bottom: 1px solid orange; 80 | border-right: 1px solid orange; 81 | } 82 | 50% { 83 | border-left: 1px solid gray; 84 | border-top: 1px solid gray; 85 | } 86 | } 87 | @keyframes pulse-top-right { 88 | 0% { 89 | border-top: 1px solid gray; 90 | border-right: 1px solid gray; 91 | } 92 | 25% { 93 | border-top: 1px solid orange; 94 | border-right: 1px solid orange; 95 | } 96 | 50% { 97 | border-left: 1px solid gray; 98 | border-top: 1px solid gray; 99 | } 100 | } 101 | @keyframes pulse-top-left { 102 | 0% { 103 | border-left: 1px solid gray; 104 | border-top: 1px solid gray; 105 | } 106 | 25% { 107 | border-left: 1px solid orange; 108 | border-top: 1px solid orange; 109 | } 110 | 50% { 111 | border-left: 1px solid gray; 112 | border-top: 1px solid gray; 113 | } 114 | } 115 | 116 | 117 | .cube__face--front { 118 | animation: pulse-top-right 8s 0s infinite, pulse-bottom-right 8s 2s infinite; 119 | } 120 | .cube__face--right { 121 | animation: pulse-top-left 8s 0s infinite, pulse-bottom-left 8s 2s infinite; 122 | } 123 | .cube__face--top { 124 | animation: pulse-bottom-right 8s 0s infinite, pulse-top-left 8s 6s infinite; 125 | } 126 | .cube__face--bottom { 127 | animation: pulse-top-right 8s 2s infinite, pulse-bottom-left 8s 4s infinite; 128 | } 129 | .cube__face--back { 130 | animation: pulse-bottom-right 8s 4s infinite, pulse-top-right 8s 6s infinite; 131 | } 132 | .cube__face--left { 133 | animation: pulse-bottom-left 8s 4s infinite, pulse-top-left 8s 6s infinite; 134 | }"]) 135 | 136 | (def root 137 | [:div.preloader 138 | style 139 | [:div.scene 140 | (let [titles? false] 141 | [:div.cube 142 | [:div.cube__face.cube__face--front (if titles? "front")] 143 | [:div.cube__face.cube__face--back (if titles? "back")] 144 | [:div.cube__face.cube__face--right (if titles? "right")] 145 | [:div.cube__face.cube__face--left (if titles? "left")] 146 | [:div.cube__face.cube__face--top (if titles? "top")] 147 | [:div.cube__face.cube__face--bottom (if titles? "bottom")]])]]) 148 | -------------------------------------------------------------------------------- /src/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require [clojure.java.io :as io] 3 | [crux-ui-server.main :as main])) 4 | 5 | (main/stop-servers) 6 | (main/-main) 7 | 8 | (comment 9 | (io/resource "/static/crux-ui/compiled/main.js") 10 | (main/handler {:uri "/static/crux-ui/compiled/main.js"})) 11 | -------------------------------------------------------------------------------- /src/public/logo.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: #eee; 3 | height: 100%; 4 | 5 | font-family: sans-serif; 6 | } 7 | 8 | *, *:after, *:before { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | 17 | margin: 0; 18 | padding: 1em; 19 | } 20 | 21 | .content { 22 | background-color: white; 23 | padding: 2rem; 24 | 25 | max-width: 960px; 26 | width: 50vw; 27 | 28 | margin: auto; 29 | 30 | box-shadow: 0 0 2em rgba(0, 0, 0, 0.4); 31 | border-radius: 5px; 32 | 33 | text-align: center; 34 | } 35 | 36 | #app { 37 | box-sizing: border-box; 38 | min-height: 50px; 39 | width: 100%; 40 | background-color: #efefef; 41 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); 42 | padding: 1em; 43 | } 44 | -------------------------------------------------------------------------------- /test/crux_console/query_analysis_test.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-console.query-analysis-test 2 | (:require [cljs.test :refer :all] 3 | [crux-ui.logic.query-analysis :as qa])) 4 | 5 | (def q 6 | '{:find [e p] 7 | :where [[e :crux.db/id] 8 | [e :d/my-attr-one p] 9 | [e :d/my-attr-two 3]]}) 10 | 11 | 12 | (deftest test-symbol-inference 13 | (let [res (qa/infer-symbol-attr-map q)] 14 | (is (= res {'e :crux.db/id, 'p :d/my-attr-one})))) 15 | 16 | 17 | ; (run-tests 'crux-console.query-analysis-test) 18 | -------------------------------------------------------------------------------- /test/crux_console/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns crux-console.test-runner 2 | (:require [cljs.test :refer :all] 3 | [crux-console.query-analysis-test])) 4 | 5 | 6 | (defn init [] 7 | (run-tests)) --------------------------------------------------------------------------------