diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 000000000..cf6a1bdf3 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,44 @@ +module.exports = { + presets: [ + [ + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + [ + "@babel/preset-react", + { + runtime: "classic", // Use classic runtime for React 16 + }, + ], + "@babel/preset-typescript", + ], + plugins: [ + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-transform-runtime", + ], + env: { + test: { + presets: [ + [ + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + [ + "@babel/preset-react", + { + runtime: "classic", + }, + ], + ], + }, + }, +}; diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..e7912e8f6 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,4 @@ +> 0.5% +last 2 versions +not op_mini all +not dead diff --git a/.ci/Dockerfile.cypress b/.ci/Dockerfile.cypress new file mode 100644 index 000000000..de9758f7e --- /dev/null +++ b/.ci/Dockerfile.cypress @@ -0,0 +1,16 @@ +FROM cypress/browsers:node18.12.0-chrome106-ff106 + +ENV APP=/usr/src/app +RUN useradd -m -d /$APP datareporter +USER datareporter + +WORKDIR $APP +COPY --chown=datareporter client $APP/client +COPY --chown=datareporter viz-lib/ $APP/viz-lib +COPY --chown=datareporter plywood $APP/plywood + +WORKDIR $APP/client +RUN npm ci && npm run build + +# Verify Cypress installation +RUN ./node_modules/.bin/cypress verify diff --git a/.ci/compose.ci.yml b/.ci/compose.ci.yml new file mode 100644 index 000000000..a793273c1 --- /dev/null +++ b/.ci/compose.ci.yml @@ -0,0 +1,56 @@ +# This configuration file is for the **development** setup. +# For a production example please refer to kubernetes or CloudRun setups + +services: + server: + build: + context: ../ + args: + skip_frontend_build: true + skip_dev_deps: "" + skip_ds_deps: "" + command: server + depends_on: + - postgres + - redis + ports: + - "5000:5000" + - "5678:5678" + environment: + PYTHONUNBUFFERED: 0 + REDASH_LOG_LEVEL: "INFO" + REDASH_REDIS_URL: "redis://redis:6379/0" + POSTGRES_PASSWORD: "FmTKs5vX52ufKR1rd8tn4MoSP7zvCJwb" + REDASH_DATABASE_URL: "postgresql://postgres:FmTKs5vX52ufKR1rd8tn4MoSP7zvCJwb@postgres/postgres" + REDASH_COOKIE_SECRET: "2H9gNG9obnAQ9qnR9BDTQUph6CbXKCzF" + REDASH_WORKER_NOTIFY_URL: "http://worker-server:5000/execute" + OPENAI_API_KEY: "${OPENAI_API_KEY}" + OLLAMA_API_URL: "http://ollama:11434" + PLYWOOD_SERVER_URL: "http://plywood:3000" + NODE_VERSION: 18 + NPM_VERSION: 9.5.1 + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379" + postgres: + image: pgautoupgrade/pgautoupgrade:latest + command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF" + ports: + - "15432:5432" + - "5432:5432" + restart: unless-stopped + environment: + POSTGRES_HOST_AUTH_METHOD: "trust" + plywood: + image: node:16.20.0-alpine + working_dir: /app + command: npm run watch-node + ports: + - "3000:3000" + - "9231:9229" + volumes: + - ./plywood:/app + environment: + - LOG_MODE=request_and_response diff --git a/.ci/compose.cypress.yml b/.ci/compose.cypress.yml new file mode 100644 index 000000000..d2727f3d3 --- /dev/null +++ b/.ci/compose.cypress.yml @@ -0,0 +1,89 @@ +x-redash-service: &redash-service + build: + context: ../ + args: + install_groups: "main" + code_coverage: ${CODE_COVERAGE} + dockerfile: Dockerfile +x-redash-environment: &redash-environment + REDASH_LOG_LEVEL: "INFO" + REDASH_REDIS_URL: "redis://redis:6379/0" + POSTGRES_PASSWORD: "FmTKs5vX52ufKR1rd8tn4MoSP7zvCJwb" + REDASH_DATABASE_URL: "postgresql://postgres:FmTKs5vX52ufKR1rd8tn4MoSP7zvCJwb@postgres/postgres" + REDASH_RATELIMIT_ENABLED: "false" + REDASH_ENFORCE_CSRF: "true" + REDASH_COOKIE_SECRET: "2H9gNG9obnAQ9qnR9BDTQUph6CbXKCzF" + DATAREPORTER_ENSURE_SCHEMA: "true" +services: + server: + <<: *redash-service + command: server + depends_on: + - postgres + - redis + - plywood + ports: + - "5000:5000" + environment: + <<: *redash-environment + PYTHONUNBUFFERED: 0 + PLYWOOD_SERVER_URL: "http://plywood:3000" + scheduler: + <<: *redash-service + command: scheduler + depends_on: + - server + environment: + <<: *redash-environment + worker: + <<: *redash-service + command: worker + depends_on: + - server + environment: + <<: *redash-environment + PYTHONUNBUFFERED: 0 + plywood: + build: + context: ../plywood + dockerfile: Dockerfile + ports: + - "3000:3000" + - "9231:9229" + environment: + - LOG_MODE=request_and_response + cypress: + ipc: host + build: + context: ../ + dockerfile: .ci/Dockerfile.cypress + depends_on: + - server + - worker + - scheduler + - plywood + environment: + CYPRESS_baseUrl: "http://server:5000" + CYPRESS_coverage: ${CODE_COVERAGE} + PERCY_TOKEN: ${PERCY_TOKEN} + PERCY_BRANCH: ${CIRCLE_BRANCH} + PERCY_COMMIT: ${CIRCLE_SHA1} + PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} + COMMIT_INFO_BRANCH: ${CIRCLE_BRANCH} + COMMIT_INFO_MESSAGE: ${COMMIT_INFO_MESSAGE} + COMMIT_INFO_AUTHOR: ${CIRCLE_USERNAME} + COMMIT_INFO_SHA: ${CIRCLE_SHA1} + COMMIT_INFO_REMOTE: ${CIRCLE_REPOSITORY_URL} + CYPRESS_PROJECT_ID: ${CYPRESS_PROJECT_ID} + CYPRESS_RECORD_KEY: ${CYPRESS_RECORD_KEY} + volumes: + - ./client/percy:/usr/src/app/client/percy + redis: + image: redis:7-alpine + restart: unless-stopped + postgres: + image: pgautoupgrade/pgautoupgrade:latest + command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF" + restart: unless-stopped + environment: + POSTGRES_HOST_AUTH_METHOD: "trust" diff --git a/.circleci/docker-compose.circle.yml b/.ci/docker-compose.circle.yml similarity index 92% rename from .circleci/docker-compose.circle.yml rename to .ci/docker-compose.circle.yml index 84ef76a82..e5689c6c4 100644 --- a/.circleci/docker-compose.circle.yml +++ b/.ci/docker-compose.circle.yml @@ -1,4 +1,4 @@ -version: '2.2' +version: "2.2" services: redash: build: ../ @@ -14,7 +14,7 @@ services: REDASH_REDIS_URL: "redis://redis:6379/0" REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" redis: - image: redis:3.0-alpine + image: redis:7-alpine restart: unless-stopped postgres: image: postgres:9.5.6-alpine diff --git a/.circleci/docker_build b/.ci/docker_build similarity index 100% rename from .circleci/docker_build rename to .ci/docker_build diff --git a/.circleci/pack b/.ci/pack similarity index 100% rename from .circleci/pack rename to .ci/pack diff --git a/.circleci/update_version b/.ci/update_version similarity index 100% rename from .circleci/update_version rename to .ci/update_version diff --git a/.circleci/Dockerfile.cypress b/.circleci/Dockerfile.cypress deleted file mode 100644 index d4e29881c..000000000 --- a/.circleci/Dockerfile.cypress +++ /dev/null @@ -1,12 +0,0 @@ -FROM cypress/browsers:chrome67 - -ENV APP /usr/src/app -WORKDIR $APP - -COPY package.json $APP/package.json -RUN npm run cypress:install > /dev/null - -COPY client/cypress $APP/client/cypress -COPY cypress.json $APP/cypress.json - -RUN ./node_modules/.bin/cypress verify diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3b3a61e1a..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,155 +0,0 @@ -version: 2.0 - -build-docker-image-job: &build-docker-image-job - docker: - - image: circleci/node:12 - steps: - - setup_remote_docker - - checkout - - run: sudo apt update - - run: sudo apt install python3-pip - - run: sudo pip3 install -r requirements_bundles.txt - - run: .circleci/update_version - - run: npm run bundle - - run: .circleci/docker_build -jobs: - backend-lint: - docker: - - image: circleci/python:3.7.0 - steps: - - checkout - - run: sudo pip install flake8 - - run: ./bin/flake8_tests.sh - backend-unit-tests: - environment: - COMPOSE_FILE: .circleci/docker-compose.circle.yml - COMPOSE_PROJECT_NAME: redash - docker: - - image: circleci/buildpack-deps:xenial - steps: - - setup_remote_docker - - checkout - - run: - name: Build Docker Images - command: | - set -x - docker-compose build --build-arg skip_ds_deps=true --build-arg skip_frontend_build=true - docker-compose up -d - sleep 10 - - run: - name: Create Test Database - command: docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests;" - - run: - name: List Enabled Query Runners - command: docker-compose run --rm redash manage ds list_types - - run: - name: Run Tests - command: docker-compose run --name tests redash tests --junitxml=junit.xml --cov-report xml --cov=redash --cov-config .coveragerc tests/ - - run: - name: Copy Test Results - command: | - mkdir -p /tmp/test-results/unit-tests - docker cp tests:/app/coverage.xml ./coverage.xml - docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml - when: always - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: coverage.xml - frontend-lint: - docker: - - image: circleci/node:12 - steps: - - checkout - - run: mkdir -p /tmp/test-results/eslint - - run: npm ci - - run: npm run lint:ci - - store_test_results: - path: /tmp/test-results - frontend-unit-tests: - docker: - - image: circleci/node:12 - steps: - - checkout - - run: sudo apt update - - run: sudo apt install python3-pip - - run: sudo pip3 install -r requirements_bundles.txt - - run: npm ci - - run: npm run bundle - - run: - name: Run App Tests - command: npm test - - run: - name: Run Visualizations Tests - command: (cd viz-lib && npm test) - - run: npm run lint - frontend-e2e-tests: - environment: - COMPOSE_FILE: .circleci/docker-compose.cypress.yml - COMPOSE_PROJECT_NAME: cypress - PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA== - CYPRESS_PROJECT_ID_ENCODED: OTI0Y2th - CYPRESS_RECORD_KEY_ENCODED: YzA1OTIxMTUtYTA1Yy00NzQ2LWEyMDMtZmZjMDgwZGI2ODgx - docker: - - image: circleci/node:12 - steps: - - setup_remote_docker - - checkout - - run: - name: Install npm dependencies - command: | - npm ci - - run: - name: Setup Redash server - command: | - npm run cypress build - npm run cypress start -- --skip-db-seed - docker-compose run cypress npm run cypress db-seed - - run: - name: Execute Cypress tests - command: npm run cypress run-ci - - run: - name: "Failure: output container logs to console" - command: | - docker-compose logs - when: on_fail - build-docker-image: *build-docker-image-job - build-preview-docker-image: *build-docker-image-job -workflows: - version: 2 - build: - jobs: - - backend-lint - - backend-unit-tests: - requires: - - backend-lint - - frontend-lint - - frontend-unit-tests: - requires: - - backend-lint - - frontend-lint - - frontend-e2e-tests: - requires: - - frontend-lint - - build-preview-docker-image: - requires: - - backend-unit-tests - - frontend-unit-tests - - frontend-e2e-tests - filters: - branches: - only: - - master - - hold: - type: approval - requires: - - backend-unit-tests - - frontend-unit-tests - - frontend-e2e-tests - filters: - branches: - only: - - /release\/.*/ - - build-docker-image: - requires: - - hold diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml deleted file mode 100644 index f058d652e..000000000 --- a/.circleci/docker-compose.cypress.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '2.2' -services: - server: - build: ../ - command: server - depends_on: - - postgres - - redis - ports: - - "5000:5000" - environment: - PYTHONUNBUFFERED: 0 - REDASH_LOG_LEVEL: "INFO" - REDASH_REDIS_URL: "redis://redis:6379/0" - REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" - REDASH_RATELIMIT_ENABLED: "false" - REDASH_ENFORCE_CSRF: "true" - scheduler: - build: ../ - command: scheduler - depends_on: - - server - environment: - REDASH_REDIS_URL: "redis://redis:6379/0" - worker: - build: ../ - command: worker - depends_on: - - server - environment: - PYTHONUNBUFFERED: 0 - REDASH_LOG_LEVEL: "INFO" - REDASH_REDIS_URL: "redis://redis:6379/0" - REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" - cypress: - build: - context: ../ - dockerfile: .circleci/Dockerfile.cypress - depends_on: - - server - - worker - - scheduler - environment: - CYPRESS_baseUrl: "http://server:5000" - PERCY_TOKEN: ${PERCY_TOKEN} - PERCY_BRANCH: ${CIRCLE_BRANCH} - PERCY_COMMIT: ${CIRCLE_SHA1} - PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} - COMMIT_INFO_BRANCH: ${CIRCLE_BRANCH} - COMMIT_INFO_MESSAGE: ${COMMIT_INFO_MESSAGE} - COMMIT_INFO_AUTHOR: ${CIRCLE_USERNAME} - COMMIT_INFO_SHA: ${CIRCLE_SHA1} - COMMIT_INFO_REMOTE: ${CIRCLE_REPOSITORY_URL} - CYPRESS_PROJECT_ID: ${CYPRESS_PROJECT_ID} - CYPRESS_RECORD_KEY: ${CYPRESS_RECORD_KEY} - redis: - image: redis:3.0-alpine - restart: unless-stopped - postgres: - image: postgres:9.5.6-alpine - command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF" - restart: unless-stopped diff --git a/.dockerignore b/.dockerignore index a43e11cdc..aa9a84dd1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,30 +1,24 @@ # Ignore general files and directories -.* .venv/ venv/ .git/ -*.md .coveragerc .coverage coverage.xml -.circleci/ .github/ +*.md +LICENSE* +docker-compose.* +compose.* .codeclimate.yml -docker-compose*.yml netlify.toml /setup/ scripts setup - -# Ignore client-specific files and directories client/.tmp/ -client/dist/ - -# Ignore Plywood server files and directories -plywood/server/node_modules/ -plywood/server/client/node_modules/ - -# Ignore Node.js dependencies -node_modules/ +client/node_modules/ +plywood/node_modules/ +plywood/client/node_modules/ viz-lib/node_modules/ .tmp/ +.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..98172568b --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# Your OpenAI API key for accessing AI services +OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# gemini api key +GEMINI_API_KEY=xxx + +# Password for the PostgreSQL database +POSTGRES_PASSWORD=xxx + +# Secret key for Redash cookies +REDASH_COOKIE_SECRET=xxx + +# API key for Mailchimp integration +MAILCHIMP_API_KEY=xxx + +# List ID for Mailchimp audience +MAILCHIMP_LIST_ID=xxx + +# Enable or disable code coverage reports (true/false) +CODE_COVERAGE=false + +# SMTP server address for Redash email notifications +REDASH_MAIL_SERVER=smtp.sendgrid.net + +# SMTP server port for Redash email notifications +REDASH_MAIL_PORT=587 + +# Use TLS for SMTP connection (true/false) +REDASH_MAIL_USE_TLS=true + +# Use SSL for SMTP connection (true/false) +REDASH_MAIL_USE_SSL=false + +# SMTP username for Redash email notifications +REDASH_MAIL_USERNAME=your_username + +# SMTP password for Redash email notifications +REDASH_MAIL_PASSWORD=your_password diff --git a/.gcloud/deploy.yaml b/.gcloud/deploy.yaml index 0f0ea6707..d44032f72 100644 --- a/.gcloud/deploy.yaml +++ b/.gcloud/deploy.yaml @@ -3,33 +3,36 @@ substitutions: steps: - name: "gcr.io/cloud-builders/gcloud" - args: [ - "-c", - "gcloud secrets versions access latest --secret=kubeconfig-digitalocean-k8s > /config/kubeconfig.conf" - ] + args: + [ + "-c", + "gcloud secrets versions access latest --secret=kubeconfig-digitalocean-k8s > /config/kubeconfig.conf", + ] entrypoint: "bash" volumes: - name: "config" path: "/config" - name: "alpine/k8s:1.26.10" - env: [ "KUBECONFIG=/config/kubeconfig.conf" ] + env: ["KUBECONFIG=/config/kubeconfig.conf"] entrypoint: "helm" - args: [ - "repo", - "add", - "datareporter", - "https://dataminelab.github.io/contrib-helm-chart/" - ] + args: + [ + "repo", + "add", + "datareporter", + "https://dataminelab.github.io/contrib-helm-chart/", + ] volumes: - name: "config" path: "/config" - name: "alpine/k8s:1.26.10" - env: [ - "KUBECONFIG=/config/kubeconfig.conf", - "PROJECT_ID=$PROJECT_ID", - "BRANCH_NAME=$BRANCH_NAME", - ] + env: + [ + "KUBECONFIG=/config/kubeconfig.conf", + "PROJECT_ID=$PROJECT_ID", + "BRANCH_NAME=$BRANCH_NAME", + ] entrypoint: "ash" args: - "-c" @@ -51,4 +54,3 @@ steps: volumes: - name: "config" path: "/config" - diff --git a/.gcloud/docker-compose.server.yaml b/.gcloud/docker-compose.server.yaml index 8e054e0b0..4578baef8 100644 --- a/.gcloud/docker-compose.server.yaml +++ b/.gcloud/docker-compose.server.yaml @@ -7,8 +7,6 @@ x-redash-service: &redash-service - ${TAG-latest} cache_from: - "eu.gcr.io/datareporter/datareporter:cache" - cache_to: - - "eu.gcr.io/datareporter/datareporter:cache" args: skip_frontend_build: "${skip_frontend_build-true}" skip_dev_deps: "${skip_dev_deps-false}" @@ -30,12 +28,14 @@ services: environment: <<: *redash-environment REDASH_WORKER_NOTIFY_URL: "http://worker-server:5000/execute" + OPENAI_API_KEY: "${OPENAI_API_KEY}" + OLLAMA_API_URL: "http://ollama:11434" PYTHONUNBUFFERED: 0 PLYWOOD_SERVER_URL: "http://plywood:3000" volumes: - .:/app redis: - image: redis:6-alpine + image: redis:7-alpine restart: unless-stopped ports: - "6379:6379" @@ -48,13 +48,11 @@ services: restart: unless-stopped plywood: build: - context: ../plywood/server + context: ../plywood tags: - ${TAG-latest} cache_from: - "eu.gcr.io/datareporter/plywood:cache" - cache_to: - - "eu.gcr.io/datareporter/plywood:cache" ports: - "3000:3000" - "9231:9229" diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 000000000..6b30b97c4 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,91 @@ +#!/bin/bash +# Pre-commit hook: check formatting on staged files +# Blocks the commit if any check fails. Fix issues and re-commit. + +# Set locale to avoid warnings +export LC_ALL=C.UTF-8 +export LANG=C.UTF-8 + +REPO_ROOT="$(git rev-parse --show-toplevel)" +FAILED=0 + +# ─── Prettier (JS/TS/CSS/JSON/MD) ──────────────────────────── + +STAGED_JS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|scss|less|md)$') + +if [ -n "$STAGED_JS" ]; then + if command -v npx &> /dev/null; then + echo "[prettier] Checking formatting..." + + # Check all files at once for efficiency + FILES_TO_CHECK="" + for FILE in $STAGED_JS; do + if [ -f "$REPO_ROOT/$FILE" ]; then + FILES_TO_CHECK="$FILES_TO_CHECK $REPO_ROOT/$FILE" + fi + done + + if [ -n "$FILES_TO_CHECK" ]; then + if ! npx prettier --config "$REPO_ROOT/.prettierrc.json" --check $FILES_TO_CHECK 2>&1 | grep -v "Checking formatting..."; then + echo "" + echo "Some files need formatting. Run 'make fmt' to fix." + FAILED=1 + fi + fi + else + echo "[prettier] npx not found, skipping" + fi +fi + +# ─── Black + Ruff (Python) ─────────────────────────────────── + +STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.py$') + +if [ -n "$STAGED_PY" ]; then + # Black — code formatter (--diff shows what it would change) + if command -v black &> /dev/null; then + echo "[black] Checking Python formatting..." + for FILE in $STAGED_PY; do + if [ -f "$REPO_ROOT/$FILE" ]; then + DIFF=$(black --diff --quiet "$REPO_ROOT/$FILE" 2>/dev/null) + if [ -n "$DIFF" ]; then + echo "" + echo "── $FILE ──" + echo "$DIFF" + FAILED=1 + fi + fi + done + else + echo "[black] black not found, skipping" + fi + + # Ruff — linter (already shows file:line:col and error code) + if command -v ruff &> /dev/null; then + echo "[ruff] Linting Python files..." + for FILE in $STAGED_PY; do + if [ -f "$REPO_ROOT/$FILE" ]; then + RUFF_OUT=$(ruff check "$REPO_ROOT/$FILE" 2>&1) + if [ $? -ne 0 ]; then + echo "" + echo "── $FILE ──" + echo "$RUFF_OUT" + FAILED=1 + fi + fi + done + else + echo "[ruff] ruff not found, skipping" + fi +fi + +if [ $FAILED -ne 0 ]; then + echo "" + echo "=========================================" + echo "Run 'make fmt' to fix and re-stage all, then re-commit." + echo "To bypass (not recommended): git commit --no-verify" + echo "=========================================" + exit 1 +fi + +exit 0 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a4e1d2521..6fdc2c1ed 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,6 @@ -## What type of PR is this? (check all applicable) - +## What type of PR is this? + + - [ ] Refactor - [ ] Feature @@ -10,6 +11,19 @@ ## Description + + +## How is this tested? + +- [ ] Unit tests (pytest, jest) +- [ ] E2E Tests (Cypress) +- [ ] Manually +- [ ] N/A + + + ## Related Tickets & Documents + + ## Mobile & Desktop Screenshots/Recordings (if there are UI changes) diff --git a/.github/config.yml b/.github/config.yml index 54a3cb00c..f6dfc7c4d 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -2,4 +2,3 @@ requestInfoLabelToAdd: needs-more-info requestInfoReplyComment: > We would appreciate it if you could provide us with more info about this issue/pr! - diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3fae9c2bf..07d231b29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,49 +1,64 @@ version: 2 updates: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + commit-message: + prefix: "(actions): " - directory: "/plywood/server" target-branch: "develop" package-ecosystem: "npm" commit-message: prefix: "(plywood-server): " schedule: - interval: 'weekly' - day: 'saturday' + interval: "weekly" + day: "saturday" ignore: - dependency-name: "readable-stream" - update-types: ["version-update:semver-any"] - - directory: "/plywood/server/client" + update-types: ["version-update:semver-minor"] + - directory: "/plywood/client" target-branch: "develop" package-ecosystem: "npm" commit-message: prefix: "(plywood-client): " ignore: - dependency-name: "readable-stream" - update-types: ["version-update:semver-any"] + update-types: ["version-update:semver-minor"] schedule: - interval: 'weekly' - day: 'saturday' + interval: "weekly" + day: "saturday" - directory: "/" target-branch: "develop" package-ecosystem: "pip" commit-message: prefix: "(python): " schedule: - interval: 'weekly' - day: 'saturday' + interval: "weekly" + day: "saturday" - directory: "/client" target-branch: "develop" package-ecosystem: "npm" commit-message: prefix: "(client-npm): " schedule: - interval: 'weekly' - day: 'saturday' + interval: "weekly" + day: "saturday" + - directory: "/client/app/components/TurniloComponent" + target-branch: "develop" + package-ecosystem: "npm" + commit-message: + prefix: "(turnilo): " + schedule: + interval: "weekly" + day: "saturday" - directory: "/viz-lib" target-branch: "develop" package-ecosystem: "npm" commit-message: prefix: "(vizlib-npm): " schedule: - interval: 'weekly' - day: 'saturday' \ No newline at end of file + interval: "weekly" + day: "saturday" diff --git a/.github/scripts/next_version.sh b/.github/scripts/next_version.sh index 74edb78ec..567ba724f 100755 --- a/.github/scripts/next_version.sh +++ b/.github/scripts/next_version.sh @@ -8,8 +8,6 @@ if [[ "${GITHUB_REF_TYPE-""}" == "tag" ]]; then elif [[ "${BRANCH-""}" == "main" ]]; then PREFIX="stable" else - - echo "not main ${BRANCH#refs/heads/-""}" PREFIX="develop" fi diff --git a/.github/support.yml b/.github/support.yml deleted file mode 100644 index 3dff3293a..000000000 --- a/.github/support.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Configuration for Support Requests - https://github.com/dessant/support-requests - -# Label used to mark issues as support requests -supportLabel: Support Question - -# Comment to post on issues marked as support requests, `{issue-author}` is an -# optional placeholder. Set to `false` to disable -supportComment: false - -# Close issues marked as support requests -close: true - -# Lock issues marked as support requests -lock: false - -# Assign `off-topic` as the reason for locking. Set to `false` to disable -setLockReason: true - -# Repository to extend settings from -# _extends: repo diff --git a/.github/weekly-digest.yml b/.github/weekly-digest.yml deleted file mode 100644 index 08cced639..000000000 --- a/.github/weekly-digest.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Configuration for weekly-digest - https://github.com/apps/weekly-digest -publishDay: mon -canPublishIssues: true -canPublishPullRequests: true -canPublishContributors: true -canPublishStargazers: true -canPublishCommits: true diff --git a/.github/workflows/analyze-test-results.yaml b/.github/workflows/analyze-test-results.yaml new file mode 100644 index 000000000..4f87c80d8 --- /dev/null +++ b/.github/workflows/analyze-test-results.yaml @@ -0,0 +1,89 @@ +name: Restyled + +on: + workflow_call: + inputs: + ref: + description: "The git reference to checkout" + required: false + type: string + default: "" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze-test-results: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifacts (for backup) + uses: actions/download-artifact@v4 + with: + name: e2e-test-results + path: test-results/ + + - name: Show downloaded artifacts + run: | + echo "📁 Downloaded artifacts for backup:" + find test-results/ -type f | head -20 + + - name: Upload Screenshots to Percy + if: always() + run: | + echo "📸 Uploading screenshots to Percy..." + + # Check what we downloaded + if [ -d "test-results/screenshots" ]; then + echo "Found screenshots, uploading to Percy..." + + # Create temporary directory for Percy + mkdir -p temp-percy + cd temp-percy + + # Initialize npm and install Percy + npm init -y + npm install @percy/cli@1.28.8 + + # Upload screenshots to Percy + PERCY_TOKEN="${{ secrets.PERCY_TOKEN }}" \ + PERCY_BRANCH="${{ github.head_ref || github.ref_name }}" \ + PERCY_COMMIT="${{ github.sha }}" \ + npx @percy/cli upload ../test-results/screenshots + else + echo "No screenshots found to upload" + fi + + - name: Add Artifact Links to PR (on failure) + if: failure() && github.event_name == 'pull_request' && needs.frontend-unit-tests.result == 'success' + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const ref = context.sha; + + // Check check-runs for this commit and skip if Restyled failed + const checks = await github.rest.checks.listForRef({ owner, repo, ref }); + const restyled = checks.data.check_runs.find(c => c.name === 'Restyled' || c.name === 'restyled'); + if (restyled && restyled.conclusion === 'failure') { + console.log('Restyled check failed — skipping artifact comment.'); + return; + } + + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const comment = `## ❌ Test Failure Detected + + 📹 **Test Videos Available**: [Download Artifacts](${runUrl}) + + Test execution failed. Videos and screenshots are available in the GitHub Actions artifacts for debugging.`; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner, + repo, + body: comment + }); diff --git a/.github/workflows/datareporter.code-workspace b/.github/workflows/datareporter.code-workspace new file mode 100644 index 000000000..63a368e49 --- /dev/null +++ b/.github/workflows/datareporter.code-workspace @@ -0,0 +1,31 @@ +{ + "folders": [ + { + "name": "client", + "path": "../../client", + }, + { + "name": "TurniloComponent", + "path": "../../client/app/components/TurniloComponent", + }, + { + "name": "viz-lib", + "path": "../../viz-lib", + }, + { + "name": "plywood's client", + "path": "../../plywood/client", + }, + { + "name": "plywood", + "path": "../../plywood", + }, + { + "name": "redash", + "path": "../../redash", + }, + ], + "settings": { + "jest.disabledWorkspaceFolders": ["TurniloComponent", "plywood"], + }, +} diff --git a/.github/workflows/periodic-snapshot.yaml b/.github/workflows/periodic-snapshot.yaml new file mode 100644 index 000000000..b50e4062a --- /dev/null +++ b/.github/workflows/periodic-snapshot.yaml @@ -0,0 +1,84 @@ +name: Periodic Snapshot + +on: + workflow_dispatch: + inputs: + bump: + description: "Bump the last digit of the version" + required: false + type: boolean + version: + description: "Specific version to set" + required: false + default: "" + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +permissions: + actions: write + contents: write + +jobs: + bump-version-and-tag: + runs-on: ubuntu-latest + if: github.ref_name == github.event.repository.default_branch + steps: + - uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.ACTION_PUSH_KEY }} + + - run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + # Function to bump the version + bump_version() { + local version="$1" + local IFS=. + read -r major minor patch <<< "$version" + patch=$((patch + 1)) + echo "$major.$minor.$patch-dev" + } + + # Determine the new version tag + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + BUMP_INPUT="${{ github.event.inputs.bump }}" + SPECIFIC_VERSION="${{ github.event.inputs.version }}" + + # Check if both bump and specific version are provided + if [ "$BUMP_INPUT" = "true" ] && [ -n "$SPECIFIC_VERSION" ]; then + echo "::error::Error: Cannot specify both bump and specific version." + exit 1 + fi + + if [ -n "$SPECIFIC_VERSION" ]; then + TAG_NAME="$SPECIFIC_VERSION-dev" + elif [ "$BUMP_INPUT" = "true" ]; then + CURRENT_VERSION=$(grep '"version":' client/package.json | awk -F\" '{print $4}') + TAG_NAME=$(bump_version "$CURRENT_VERSION") + else + echo "No version bump or specific version provided for manual dispatch." + exit 1 + fi + else + TAG_NAME="$(date +%y.%m).0-dev" + fi + + echo "New version tag: $TAG_NAME" + + # Update version in files + gawk -i inplace -F: -v q=\" -v tag=${TAG_NAME} '/^ "version": / { print $1 FS, q tag q ","; next} { print }' client/package.json + gawk -i inplace -F= -v q=\" -v tag=${TAG_NAME} '/^__version__ =/ { print $1 FS, q tag q; next} { print }' redash/__init__.py + gawk -i inplace -F= -v q=\" -v tag=${TAG_NAME} '/^version =/ { print $1 FS, q tag q; next} { print }' pyproject.toml + + git add client/package.json redash/__init__.py pyproject.toml + git commit -m "Snapshot: ${TAG_NAME}" + git tag ${TAG_NAME} + git push --atomic origin develop refs/tags/${TAG_NAME} + + # Run the 'preview-image' workflow if run this workflow manually + # For more information, please see the: https://docs.github.com/en/actions/security-guides/automatic-token-authentication + if [ "$BUMP_INPUT" = "true" ] || [ -n "$SPECIFIC_VERSION" ]; then + gh workflow run preview-image.yml --ref $TAG_NAME + fi diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml deleted file mode 100644 index f64bca683..000000000 --- a/.github/workflows/pre-merge.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: Pre-merge CI -on: - workflow_call: - pull_request: - branches: - - develop - -concurrency: - cancel-in-progress: true - group: ${{ github.workflow }}-${{ github.ref }} - -jobs: - unit_tests: - uses: ./.github/workflows/test-unit.yaml - secrets: inherit diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e379924e4..80e22878c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,13 +10,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: - unit_tests: - uses: ./.github/workflows/test-unit.yaml - secrets: inherit - release_image: + get_version: runs-on: ubuntu-latest - needs: - - unit_tests + outputs: + version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v4 with: @@ -30,6 +27,14 @@ jobs: next_version=$(./.github/scripts/next_version.sh) echo "Next version: $next_version" echo "version=$next_version" >> $GITHUB_OUTPUT + build_datareporter: + runs-on: ubuntu-latest + needs: + - get_version + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Docker login uses: docker/login-action@v3 with: @@ -40,19 +45,44 @@ jobs: uses: docker/build-push-action@v6 with: push: true - tags: europe-west1-docker.pkg.dev/datareporter/datareporter/datareporter:${{ steps.get-version.outputs.version }} + tags: europe-west1-docker.pkg.dev/datareporter/datareporter/datareporter:${{needs.get_version.outputs.version}} + build-args: version="${{needs.get_version.outputs.version}}" + build_plywood: + runs-on: ubuntu-latest + needs: + - get_version + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Docker login + uses: docker/login-action@v3 + with: + registry: ${{ secrets.GCR_URL }} + username: ${{ secrets.GCR_USERNAME }} + password: ${{ secrets.GCR_PASSWORD }} - name: Build and push plywood uses: docker/build-push-action@v6 with: push: true - context: plywood/server - tags: europe-west1-docker.pkg.dev/datareporter/datareporter/plywood:${{ steps.get-version.outputs.version }} + context: plywood + tags: europe-west1-docker.pkg.dev/datareporter/datareporter/plywood:${{needs.get_version.outputs.version}} + build-args: version="${{needs.get_version.outputs.version}}" + tag_release: + runs-on: ubuntu-latest + needs: + - build_datareporter + - build_plywood + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Tag release shell: bash run: | - if ! [ $(git rev-list ${{ steps.get-version.outputs.version }} >/dev/null )]; + if ! [ $(git rev-list ${{needs.get_version.outputs.version}} >/dev/null )]; then - git tag ${{ steps.get-version.outputs.version }} + git tag ${{needs.get_version.outputs.version}} git push --tags fi diff --git a/.github/workflows/restyled.yaml b/.github/workflows/restyled.yaml new file mode 100644 index 000000000..f478dd990 --- /dev/null +++ b/.github/workflows/restyled.yaml @@ -0,0 +1,45 @@ +name: Restyled + +on: + workflow_call: + inputs: + ref: + description: "The git reference to checkout" + required: false + type: string + default: "" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + restyled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha }} + + - uses: restyled-io/actions/setup@v4 + + - id: restyler + uses: restyled-io/actions/run@v4 + env: + BLACK_NUM_WORKERS: "1" + with: + fail-on-differences: true + + - if: | + !cancelled() && + steps.restyler.outputs.success == 'true' && + github.event.pull_request.head.repo.full_name == github.repository + uses: peter-evans/create-pull-request@v6 + with: + base: ${{ steps.restyler.outputs.restyled-base }} + branch: ${{ steps.restyler.outputs.restyled-head }} + title: ${{ steps.restyler.outputs.restyled-title }} + body: ${{ steps.restyler.outputs.restyled-body }} + labels: "restyled" + reviewers: ${{ github.event.pull_request.user.login }} + delete-branch: true diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index a7be698f9..b709c1c8b 100644 --- a/.github/workflows/test-unit.yaml +++ b/.github/workflows/test-unit.yaml @@ -1,69 +1,90 @@ -name: "Unit tests" +name: "Unit Tests" on: - workflow_call: - + push: + branches: + - main + pull_request: + branches: [main, develop] +env: + NODE_VERSION: 18 + NPM_VERSION: 9.5.1 + REDASH_COOKIE_SECRET: "${secrets.REDASH_COOKIE_SECRET}" permissions: contents: read pull-requests: write + checks: write jobs: - PythonLint: + restyled: + if: github.event_name == 'pull_request' + uses: ./.github/workflows/restyled.yaml + with: + ref: ${{ github.event.pull_request.head.sha }} + backend-lint: timeout-minutes: 5 runs-on: ubuntu-latest + needs: restyled steps: + - if: github.event.pull_request.mergeable == 'false' + name: Exit if PR is not mergeable + run: exit 1 - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-python@v5 with: - python-version: '3.8' - - name: Install pip - run: | - python -m ensurepip --upgrade - - shell: bash - run: |- - pip3 install flake8 - - shell: bash - run: |- - export PATH=$PATH:/builder/home/.local/bin - ./bin/flake8_tests.sh - PythonUnitTests: + python-version: "3.10" + - run: python -m ensurepip --upgrade + - run: sudo pip install black==26.3.1 ruff + - run: ruff check . + - run: black --check . + backend-unit-tests: timeout-minutes: 60 runs-on: ubuntu-latest + needs: backend-lint + env: + COMPOSE_FILE: .ci/compose.ci.yml + COMPOSE_PROJECT_NAME: server + COMPOSE_DOCKER_CLI_BUILD: 1 + DOCKER_BUILDKIT: 1 steps: + - if: github.event.pull_request.mergeable == 'false' + name: Exit if PR is not mergeable + run: exit 1 - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - version: v0.12.1 - - name: Build docker compose - uses: docker/bake-action@v4 - with: - files: docker-compose.yml, docker-compose.ci.yml - load: true - - name: "Prepare test environment" - shell: bash - run: |- - docker compose -f docker-compose.ci.yml up -d postgres redis - - name: "Wait for postgres to start" - shell: bash - run: |- - for i in 1 2 3 4 5; do docker compose -f docker-compose.ci.yml run --rm postgres psql -h postgres -U postgres -c "SELECT 1 FROM pg_database WHERE datname = 'tests'" && break || sleep 2; done - - name: "Create tests database" - shell: bash - run: |- - docker compose -f docker-compose.ci.yml run --rm postgres psql -h postgres -U postgres -c "create database tests;" - - name: "List Enabled Query Runners" - shell: bash - run: |- - docker compose -f docker-compose.ci.yml run --rm server manage ds list_types - - name: "Execute unit tests" + version: v0.17.0 + - name: Build docker Images + run: | + set -x + docker compose build --build-arg install_groups="main,all_ds,dev" --build-arg skip_frontend_build=true + docker compose up -d + sleep 10 + - name: Wait for Postgres to be healthy + run: | + for i in {1..30}; do + if docker compose ps | grep postgres | grep -q "healthy"; then + echo "Postgres is healthy." + break + fi + echo "Waiting for Postgres to be healthy..." + sleep 2 + done + - name: Create Test Database + run: docker compose -p server exec -T postgres psql -h localhost -U postgres -c "create database tests;" + - name: List Enabled Query Runners + run: docker compose -p server run --rm server manage ds list_types + - name: Run Tests shell: bash run: |- - docker compose -f docker-compose.ci.yml run --user 0 --name tests server tests --junitxml=junit.xml --cov-report xml --cov=redash --cov-config .coveragerc tests/ - - name: "Extract test results" + docker compose -p server run --user 0 --name tests server tests --junitxml=junit.xml --cov-report=xml --cov=redash --cov-config=.coveragerc tests/ + - name: Copy Test Results shell: bash if: always() run: | @@ -92,10 +113,455 @@ jobs: hide_complexity: true indicators: true output: both - thresholds: '60 80' + thresholds: "60 80" - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 if: github.event_name == 'pull_request' with: recreate: true path: code-coverage-results.md + frontend-lint: + if: github.event_name == 'pull_request' + timeout-minutes: 60 + runs-on: ubuntu-latest + needs: restyled + steps: + - if: github.event.pull_request.mergeable == 'false' + name: Exit if PR is not mergeable + run: exit 1 + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: client/package-lock.json + - name: Install Dependencies + working-directory: client + run: npm ci + - name: Run Lint + working-directory: client + run: npm run lint + - name: Store Test Results + uses: actions/upload-artifact@v4 + with: + name: frontend-test-results + path: /tmp/test-results + frontend-unit-tests: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + needs: frontend-lint + steps: + - if: github.event.pull_request.mergeable == 'false' + name: Exit if PR is not mergeable + run: exit 1 + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Cache npm dependencies + id: cache-npm + uses: actions/cache@v4 + with: + path: client/node_modules + key: Linux-node-${{ hashFiles('client/package-lock.json') }} + restore-keys: Linux-node- + - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + name: Install App dependencies + working-directory: client + run: npm ci + - name: List the state of node modules + working-directory: client + run: npm list + - name: Build App + working-directory: client + run: npm run build + - name: Run App Tests + working-directory: client + run: npm run test:coverage + - name: Run Visualizations Tests + working-directory: viz-lib + run: npm run test + - name: Install plywood-server dependencies + working-directory: plywood + run: npm ci + - name: Run plywood-server Tests + working-directory: plywood + run: npm run test + - name: Install plywood-client dependencies + working-directory: plywood/client + run: npm ci + - name: Run plywood-client Tests + working-directory: plywood/client + run: npm run test + - name: turnilo dependencies install + working-directory: client/app/components/TurniloComponent + run: npm ci + - name: turnilo client tests + working-directory: client/app/components/TurniloComponent + run: npm run test:client + - name: turnilo common tests + working-directory: client/app/components/TurniloComponent + run: npm run test:common + frontend-e2e-tests: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + needs: [frontend-unit-tests, backend-unit-tests] + env: + COMPOSE_FILE: .ci/compose.cypress.yml + COMPOSE_PROJECT_NAME: cypress + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 + COMPOSE_BAKE: true + # SECURITY: Secrets moved to step-level env to limit exposure during npm install. + # Only steps that actually need secrets should reference them. + PERCY_PARALLEL_TOTAL: 1 + PERCY_PARALLEL_NONCE: ${{ github.run_id }} + PERCY_PARALLEL: true + PERCY_BRANCH: ${{ github.head_ref }} + PERCY_COMMIT: ${{ github.sha }} + PERCY_PULL_REQUEST: ${{ github.event.pull_request.number }} + COMMIT_INFO_BRANCH: ${{ github.head_ref }} + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + COMMIT_INFO_AUTHOR: ${{ github.event.pull_request.user.login }} + COMMIT_INFO_SHA: ${{ github.sha }} + PERCY_LOGLEVEL: debug + steps: + - name: Free up disk space + run: | + echo "Before cleanup:" + df -h + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + sudo apt-get clean + echo "After cleanup:" + df -h + - if: github.event.pull_request.mergeable == 'false' + name: Exit if PR is not mergeable + run: exit 1 + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Enable Code Coverage Report For Develop Branch + if: endsWith(github.ref, '/develop') || endsWith(github.ref, '/main') + run: | + echo "CODE_COVERAGE=true" >> "$GITHUB_ENV" + - name: Install Dependencies + working-directory: client + run: | + npm ci + npm install --save-dev @percy/cypress @percy/cli + - name: Check and Install Cypress Binary + run: | + if npx cypress verify; then + echo "Cypress is already installed." + else + echo "Cypress not found, installing..." + npx cypress install --force + fi + - name: Ensure .ci/compose.cypress.yml exists in client + run: | + if [ ! -f client/.ci/compose.cypress.yml ] && [ -f .ci/compose.cypress.yml ]; then + mkdir -p client/.ci + cp .ci/compose.cypress.yml client/.ci/compose.cypress.yml + fi + - name: Setup Datareporter Server + run: docker build -f .ci/Dockerfile.cypress -t cypress . + - name: Setup Cypress via npm scripts + working-directory: client + env: + # Secrets set at step-level only, not job-level, to prevent exposure during npm install + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + # Use intermediate env vars to prevent script injection from PR metadata + SAFE_COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message }} + SAFE_COMMIT_INFO_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }} + SAFE_PERCY_BRANCH: ${{ github.head_ref || github.ref_name }} + SAFE_PERCY_COMMIT: ${{ github.sha }} + SAFE_PERCY_PR: ${{ github.event.pull_request.number }} + SAFE_PERCY_NONCE: ${{ github.run_id }} + run: | + set -x + npm run cypress build + npm run cypress start -- --skip-db-seed + docker compose run \ + -e PERCY_TOKEN="$PERCY_TOKEN" \ + -e PERCY_BRANCH="$SAFE_PERCY_BRANCH" \ + -e PERCY_COMMIT="$SAFE_PERCY_COMMIT" \ + -e PERCY_PULL_REQUEST="$SAFE_PERCY_PR" \ + -e PERCY_PARALLEL_TOTAL=1 \ + -e PERCY_PARALLEL_NONCE="$SAFE_PERCY_NONCE" \ + -e PERCY_PARALLEL=true \ + -e PERCY_LOGLEVEL=debug \ + -e COMMIT_INFO_BRANCH="$SAFE_PERCY_BRANCH" \ + -e COMMIT_INFO_MESSAGE="$SAFE_COMMIT_INFO_MESSAGE" \ + -e COMMIT_INFO_AUTHOR="$SAFE_COMMIT_INFO_AUTHOR" \ + -e COMMIT_INFO_SHA="$SAFE_PERCY_COMMIT" \ + cypress npm run cypress db-seed + sleep 10 + - name: Execute Cypress Tests + working-directory: client + env: + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + run: | + set +e # Don't exit on error, we want to capture logs first + + echo "Starting Cypress tests..." + + # Create directory for logs + mkdir -p ../test-artifacts + + # Run Cypress and save output to file first + # Use env vars instead of direct expression interpolation to prevent injection + docker compose run \ + -e CYPRESS_PROJECT_ID="$CYPRESS_PROJECT_ID" \ + -e CYPRESS_RECORD_KEY="$CYPRESS_RECORD_KEY" \ + -e CYPRESS_baseUrl="http://server:5000" \ + cypress \ + bash -c " + echo '=== Starting Cypress Test Execution ===' + echo 'Current directory:' \$(pwd) + echo 'Available test files:' + find cypress/e2e -name '*.cy.js' | head -10 + echo '' + echo '=== Running Cypress Tests ===' + npx cypress run --reporter spec --browser chrome --record + exit \$? + " > ../test-artifacts/cypress-logs.txt 2>&1 + + test_exit_code=$? + + # Always display the captured output regardless of exit code + echo "" + echo "Cypress test output:" + if [ -f ../test-artifacts/cypress-logs.txt ]; then + cat ../test-artifacts/cypress-logs.txt + else + echo "No test output file found" + fi + echo "" + echo "Test execution completed with exit code: $test_exit_code" + + if [ $test_exit_code -eq 0 ]; then + echo "Tests passed" + else + echo "Tests failed with exit code: $test_exit_code" + exit 1 + fi + - name: Capture Test Artifacts + if: always() + working-directory: client + run: | + echo "Capturing test artifacts..." + + # Ensure artifacts directory exists + mkdir -p ../test-artifacts + + # Try to get artifacts from any cypress containers + CONTAINER_NAMES=$(docker ps -a --filter "name=cypress" --format "{{.Names}}") + + if [ ! -z "$CONTAINER_NAMES" ]; then + for CONTAINER_NAME in $CONTAINER_NAMES; do + echo "Attempting to extract artifacts from: $CONTAINER_NAME" + + # Copy test artifacts (ignore errors if paths don't exist) + docker cp $CONTAINER_NAME:/usr/src/app/client/cypress/screenshots ../test-artifacts/ 2>/dev/null && echo "Screenshots copied" || echo "No screenshots found" + docker cp $CONTAINER_NAME:/usr/src/app/client/cypress/videos ../test-artifacts/ 2>/dev/null && echo "Videos copied" || echo "No videos found" + docker cp $CONTAINER_NAME:/usr/src/app/client/cypress/reports ../test-artifacts/ 2>/dev/null && echo "Reports copied" || echo "No reports found" + + # Only copy container logs if we don't already have captured logs + if [ ! -f ../test-artifacts/cypress-logs.txt ]; then + docker logs $CONTAINER_NAME > ../test-artifacts/cypress-container-logs.txt 2>&1 && echo "Container logs copied" || echo "No container logs" + fi + done + else + echo "No cypress containers available for artifact extraction" + fi + + # Show what we captured + echo "Final captured artifacts:" + find ../test-artifacts -type f 2>/dev/null | while read file; do + size=$(du -h "$file" 2>/dev/null | cut -f1) + echo " $file ($size)" + done || echo "No artifacts captured" + + - name: Upload Test Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results + path: test-artifacts/ + retention-days: 7 + + - name: Analyze and Comment on Test Results + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + let comment = '## E2E Test Results Analysis\n\n'; + let testsPassed = true; + let totalTests = 0; + let passCount = 0; + let failCount = 0; + + try { + const resultsDir = './test-artifacts'; + if (fs.existsSync(resultsDir)) { + const logFile = path.join(resultsDir, 'cypress-logs.txt'); + if (fs.existsSync(logFile)) { + try { + const logContent = fs.readFileSync(logFile, 'utf8'); + const lines = logContent.split('\n'); + + let overallFailedPattern = null; + let passCount = 0; + let failCount = 0; + let totalTests = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const overallMatch = line.match(/(\d+)\s+of\s+(\d+)\s+failed/i); + if (overallMatch && !overallFailedPattern) { + overallFailedPattern = { + failed: parseInt(overallMatch[1]), + total: parseInt(overallMatch[2]), + line: line.trim() + }; + } + const passMatch = line.match(/(\d+)\s+passing/); + if (passMatch) passCount += parseInt(passMatch[1]); + const failMatch = line.match(/(\d+)\s+failing/); + if (failMatch) failCount += parseInt(failMatch[1]); + } + totalTests = passCount + failCount; + if (overallFailedPattern) { + failCount = overallFailedPattern.failed; + totalTests = overallFailedPattern.total; + passCount = totalTests - failCount; + } + + testsPassed = failCount === 0; + + comment += '### '; + if (testsPassed) { + comment += 'Test Status: PASSED\n\n'; + if (totalTests > 0) { + comment += `**All ${totalTests} tests passed!**\n\n`; + } + } else { + comment += 'Test Status: FAILED\n\n'; + if (totalTests > 0) { + comment += `**${failCount} of ${totalTests} tests failed** (${passCount} passed)\n\n`; + } else if (passCount > 0 || failCount > 0) { + comment += `**${failCount} test(s) failed**, ${passCount} test(s) passed\n\n`; + } + } + + comment += '**Test Statistics:**\n'; + comment += `- Total Tests: ${totalTests || 'Unknown'}\n`; + comment += `- Passed: ${passCount}\n`; + comment += `- Failed: ${failCount}\n`; + if (totalTests > 0) { + const passPercentage = Math.round((passCount / totalTests) * 100); + comment += `- Pass Rate: ${passPercentage}%\n`; + } + comment += '\n'; + + if (failCount > 0) { + const failedSpecs = new Set(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const runningMatch = line.match(/Running:\s+(.+\.cy\.js)/); + if (runningMatch) { + for (let j = i + 1; j < Math.min(i + 50, lines.length); j++) { + if (lines[j].match(/\d+\s+failing/) || lines[j].includes('✖')) { + failedSpecs.add(runningMatch[1]); + break; + } + } + } + } + + if (failedSpecs.size > 0) { + comment += '**Failed Spec Files:**\n'; + Array.from(failedSpecs).slice(0, 15).forEach((spec) => { + comment += `- ${spec}\n`; + }); + if (failedSpecs.size > 15) { + comment += `- ... and ${failedSpecs.size - 15} more\n`; + } + comment += '\n'; + } + } + + } catch (e) { + comment += `Error parsing logs: ${e.message}\n\n`; + } + } else { + comment += 'No Cypress logs found\n\n'; + } + + comment += '**Test Artifacts:**\n'; + const videoDir = path.join(resultsDir, 'videos'); + const screenshotDir = path.join(resultsDir, 'screenshots'); + let videoCount = 0; + let screenshotCount = 0; + + if (fs.existsSync(videoDir)) { + videoCount = fs.readdirSync(videoDir, { recursive: true }) + .filter(file => file.endsWith('.mp4')).length; + if (videoCount > 0) { + comment += `- ${videoCount} test video(s) recorded\n`; + } + } + + if (fs.existsSync(screenshotDir)) { + screenshotCount = fs.readdirSync(screenshotDir, { recursive: true }) + .filter(file => file.match(/\.(png|jpg|jpeg)$/)).length; + if (screenshotCount > 0) { + comment += `- ${screenshotCount} screenshot(s) captured\n`; + } + } + + if (videoCount === 0 && screenshotCount === 0) { + comment += '- No additional artifacts captured\n'; + } + + } else { + comment += '### No Test Artifacts Found\n'; + comment += 'Tests did not run or artifacts were not saved.\n'; + } + + } catch (error) { + comment += `### Error Analyzing Results\n${error.message}\n`; + } + + comment += '\n---\n*Generated from E2E test artifacts*'; + + if (!testsPassed) { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } + analyze-test-results: + if: always() && github.event_name == 'pull_request' + uses: ./.github/workflows/analyze-test-results.yaml + with: + ref: ${{ github.event.pull_request.head.sha }} + needs: frontend-e2e-tests diff --git a/.gitignore b/.gitignore index f93fa46f4..97caca0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ coverage.xml \#*# *~ _build -.vscode .env dump.rdb @@ -29,3 +28,14 @@ client/cypress/videos dist scripts/config.js +bin/act +ollama-models + +# Test artifacts +coverage/ +client/coverage/ +test-results/ +.nyc_output/ +*.lcov +junit.xml +jest-junit.xml \ No newline at end of file diff --git a/.node-version b/.node-version deleted file mode 100644 index f46d5e394..000000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -14.21.3 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..0828ab794 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18 \ No newline at end of file diff --git a/.percy.yml b/.percy.yml new file mode 100644 index 000000000..98afc5038 --- /dev/null +++ b/.percy.yml @@ -0,0 +1,7 @@ +version: 2 +discovery: + concurrency: 10 +snapshot: + widths: + - 375 + - 1280 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..9a82f31ef --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "arrowParens": "avoid", + "singleQuote": false, + "trailingComma": "all", + "quoteProps": "consistent" +} diff --git a/.python-version b/.python-version deleted file mode 100644 index 4351a7e3a..000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.8.7 diff --git a/.restyled.yaml b/.restyled.yaml index 9a9537ce7..1c6e3e7e5 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -18,46 +18,30 @@ statuses: error: true # Request review on the Restyle PR? -# -# Possible values: -# -# author: From the author of the original PR -# owner: From the owner of the repository -# none: Don't -# -# One value will apply to both origin and forked PRs, but you can also specify -# separate values. -# -# request_review: -# origin: author -# forked: owner -# request_review: author # Add labels to any created Restyle PRs -# -# These can be used to tell other automation to avoid our PRs. -# -labels: ["Skip CI"] +labels: + - restyled + - "Skip CI" -# Labels to ignore -# -# PRs with any of these labels will be ignored by Restyled. -# -# ignore_labels: -# - restyled-ignore - -# Restylers to run, and how restylers: - name: black - image: restyled/restyler-black:v19.10b0 + image: restyled/restyler-black:v24.4.2 include: - redash - tests - migrations/versions - name: prettier - image: restyled/restyler-prettier:v1.19.1-2 + image: restyled/restyler-prettier:v3.3.2-2 + command: + - prettier + - --write + - --config + - .prettierrc.json include: - client/app/**/*.js - client/app/**/*.jsx - client/cypress/**/*.js + - plywood/**/*.js + - plywood/**/*.ts diff --git a/.tool-versions b/.tool-versions index 3febf4d24..504f142b4 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -python 3.8.7 +python 3.11 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..35dcbd06b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,111 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Server Debugger: Remote Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "name": "Worker Debugger: Remote Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5679 + }, + // bash bin/rest-worker --debug-port 5679 + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "type": "node", + "request": "attach", + "name": "Attach jest or mocha", + "port": 9229 + // plywood/client + // node --inspect-brk node_modules/.bin/mocha test/expression/* + + // client + // npm run test:watch + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Plywood Server", + "port": 9229, + "address": "localhost", + "cwd": "${workspaceFolder}/plywood", + "restart": true, + "smartStep": true, + "autoAttachChildProcesses": true, + "localRoot": "${workspaceFolder}/plywood", + "remoteRoot": "${workspaceFolder}/plywood", + "sourceMaps": true, + "resolveSourceMapLocations": [ + "${workspaceFolder}/plywood/**/*.js", + "${workspaceFolder}/plywood/node_modules/reporter-plywood/**/*.js" + ], + "sourceMapPathOverrides": { + "webpack:///./*": "${workspaceFolder}/plywood/*", + "webpack:///*": "${workspaceFolder}/plywood/*", + "../../src/*": "${workspaceFolder}/plywood/client/src/*", + "../src/*": "${workspaceFolder}/plywood/client/src/*", + "src/*": "${workspaceFolder}/plywood/client/src/*", + "*/node_modules/reporter-plywood/src/*": "${workspaceFolder}/plywood/client/src/*", + "/src/*": "${workspaceFolder}/plywood/src/*", + "/*": "${workspaceFolder}/plywood/*" + }, + "skipFiles": [ + "/**" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Jest Debug Current File", + "program": "${workspaceFolder}/client/node_modules/.bin/jest", + "args": ["--runInBand", "${relativeFile}"], + "cwd": "${workspaceFolder}/client", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "env": { + "TZ": "Africa/Khartoum" + } + } + ], + "compounds": [ + { + "name": "Python: Remote", + "configurations": ["Server Debugger: Remote Attach"] + }, + { + "name": "Node: Attach", + "configurations": ["Attach jest or mocha"] + } + ], + "keybindings": [ + { + "command": "workbench.action.debug.reloadWindow", + "key": "ctrl+shift+f5", + "when": "inDebugRepl" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..56f1d53cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "eslint.workingDirectories": [{ "mode": "auto" }, "./client"], + "python.analysis.diagnosticSeverityOverrides": { + "reportGeneralTypeIssues": "warning", + "reportAttributeAccessIssue": "warning", + "reportCallIssue": "warning", + "reportArgumentType": "warning", + "reportOptionalMemberAccess": "warning", + "reportIndexIssue": "warning", + "reportOperatorIssue": "warning", + "reportPrivateUsage": "warning", + "reportUnboundVariable": "warning", + "reportMissingTypeStubs": "warning", + "reportUnknownMemberType": "none", + "reportUnknownVariableType": "information" + }, + "githubPullRequests.ignoredPullRequestBranches": ["develop"], + "python-envs.defaultEnvManager": "ms-python.python:poetry", + "python-envs.defaultPackageManager": "ms-python.python:poetry", + "chat.tools.terminal.autoApprove": { + "./node_modules/.bin/mocha": true + } +} diff --git a/.zshrc b/.zshrc index 0eedb58e6..09da8c69f 100644 --- a/.zshrc +++ b/.zshrc @@ -1,6 +1,5 @@ export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" -#eval "$(pyenv init -)" eval "$(pyenv init - --path)" eval "$(pyenv virtualenv-init -)" # activate virtual environment diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..1a69994a0 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,677 @@ +# DataReporter Architecture + +> A merge of Redash (SQL-based BI) and Turnilo (OLAP cube exploration) into a unified data visualization platform. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Technology Stack](#technology-stack) +3. [Directory Structure](#directory-structure) +4. [Architecture Components](#architecture-components) +5. [Data Flow](#data-flow) +6. [API Reference](#api-reference) +7. [Configuration](#configuration) +8. [Development](#development) +9. [Key Files Reference](#key-files-reference) + +--- + +## Overview + +DataReporter combines two major BI paradigms: + +| Paradigm | Origin | Use Case | +| -------------------- | ------- | ------------------------------------------------------- | +| **SQL Queries** | Redash | Write SQL, execute against databases, visualize results | +| **OLAP Exploration** | Turnilo | Drag-drop dimensions/measures, automatic SQL generation | + +The integration allows users to switch between SQL-based queries (traditional Redash) and OLAP exploration (Turnilo), with automatic SQL translation via the Plywood server. + +### High-Level Architecture + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ Frontend (React) │ +│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ +│ │ Query Editor │ │ Dashboards │ │ TurniloComponent │ │ +│ │ (SQL) │ │ (Widgets) │ │ (OLAP UI v1.40.5) │ │ +│ └──────┬───────┘ └──────┬───────┘ └───────────┬─────────────┘ │ +└─────────┼─────────────────┼──────────────────────┼─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ Backend API (Flask) │ +│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ +│ │ /api/queries │ │/api/dashboards│ │ /api/reports │ │ +│ └──────┬───────┘ └──────┬───────┘ └───────────┬─────────────┘ │ +└─────────┼─────────────────┼──────────────────────┼─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Query Execution Layer │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Query Runners (40+) │ │ +│ │ PostgreSQL │ MySQL │ BigQuery │ Athena │ Druid │ ... │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ + │ ▼ + │ ┌─────────────────────────┐ + │ │ Plywood Server │ + │ │ (TypeScript/Express) │ + │ │ Query Translation │ + │ │ Hash ↔ SQL │ + │ └─────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Data Sources │ +│ PostgreSQL │ MySQL │ BigQuery │ Snowflake │ Druid │ Athena │ JSON │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Technology Stack + +### Backend (Python/Flask) + +| Component | Technology | Version | +| ----------- | ---------------------------- | -------------------- | +| Framework | Flask | 2.3.2 | +| Language | Python | 3.10 | +| ORM | SQLAlchemy | 1.3.24 | +| Database | PostgreSQL | (psycopg2 2.9.6) | +| Cache/Queue | Redis | 4.6.0 | +| Task Queue | RQ (Redis Queue) | 1.16.2 | +| HTTP Server | Gunicorn | 22.0.0 | +| API | Flask-RESTful | 0.3.10 | +| Auth | Flask-Login, Authlib, PyJWT | 0.6.0, 0.15.5, 2.4.0 | +| Security | Flask-Talisman, Cryptography | 0.7.0, 43.0.1 | +| Monitoring | Sentry-SDK, StatsD | 2.8.0, 3.3.0 | +| AI | google-genai | ^1.56.0 | + +### Frontend (React/TypeScript) + +| Component | Technology | Version | +| ------------- | ---------------------- | --------- | +| Framework | React | >=16.14.0 | +| Language | TypeScript/JSX | - | +| Build | Webpack | - | +| UI Library | Ant Design | 4.4.3 | +| Visualization | D3, Plotly.js, Leaflet | - | +| Testing | Jest, Cypress | - | +| Code Quality | ESLint, Prettier | - | + +### Plywood Server (TypeScript/Node.js) + +| Component | Technology | Version | +| --------------- | ------------- | ----------- | +| Runtime | Node.js | 18.20 | +| Framework | Express | 4.19.2 | +| Query Engine | Druid Toolkit | 0.19.1 | +| Data Structures | Immutable.js | 4.0.0-rc.14 | +| Monitoring | Sentry | 7.119.0 | + +### Turnilo Component + +| Component | Technology | Version | +| --------- | ---------------------------- | ------- | +| Framework | TypeScript/React | - | +| Origin | Allegro Turnilo (hardforked) | 1.40.5 | +| License | Apache 2.0 | - | + +### Data Source Drivers + +**SQL Databases:** PostgreSQL, MySQL, MS SQL Server, Druid, Athena, BigQuery, Snowflake, Oracle, Vertica, Trino/Presto + +**Cloud:** Azure Kusto, Google Analytics, AWS (Boto3) + +**NoSQL:** MongoDB, DynamoDB, Cassandra, Elasticsearch + +**Other:** Pinot, Databend, JSON + +--- + +## Directory Structure + +``` +datareporter/ +├── redash/ # Backend API and core logic (Flask) +│ ├── __init__.py # Global init: Redis, RQ, Mail, Limiter +│ ├── app.py # Flask app factory +│ ├── wsgi.py # Production WSGI entry +│ ├── models/ # SQLAlchemy ORM models +│ ├── handlers/ # REST API endpoints +│ ├── query_runner/ # Data source connectors (40+) +│ ├── plywood/ # Plywood/Turnilo integration bridge +│ ├── tasks/ # RQ background jobs +│ ├── services/ # Business logic layer +│ ├── serializers/ # JSON serialization +│ ├── authentication/ # Auth strategies +│ ├── settings/ # Configuration (400+ vars) +│ └── cli/ # CLI commands +│ +├── client/ # Frontend (React) +│ ├── app/ +│ │ ├── index.js # React entry point +│ │ ├── components/ # React components +│ │ │ └── TurniloComponent/ # OLAP UI (Turnilo v1.40.5) +│ │ ├── pages/ # Page-level components +│ │ ├── services/ # API clients +│ │ └── lib/ # Shared utilities +│ ├── cypress/ # E2E tests +│ └── package.json # Frontend dependencies +│ +├── plywood/ # OLAP query translation server +│ └── src/ +│ ├── app.ts # Express app setup +│ ├── server.ts # Server entry (port 3000) +│ ├── endpoint/ # API endpoints +│ │ ├── plywood-endpoint.ts +│ │ ├── attributes-formatter.ts +│ │ └── hash-converter.ts +│ └── formatter/ # Database-specific parsers +│ └── attributesFormatter/parsers/ +│ ├── PostgresAttributeParser.ts +│ ├── MySqlAttributeParser.ts +│ ├── BigQueryParser.ts +│ └── ... +│ +├── viz-lib/ # Visualization library (React) +│ └── src/ # Chart components +│ +├── migrations/ # Alembic database migrations +├── tests/ # Backend test suite +├── kubernetes/ # K8s deployment configs +├── bin/ # Entry scripts +│ └── docker-entrypoint # Container entry point +├── worker/ # Supervisor config for workers +├── scripts/ # Build utilities +│ +├── Dockerfile # Multi-stage container build +├── docker-compose.yml # Development services +├── compose.dev.yml # Dev-specific overrides +├── pyproject.toml # Python dependencies (Poetry) +├── Makefile # Build commands +└── manage.py # CLI entry point +``` + +--- + +## Architecture Components + +### Backend (Flask/Python) + +#### API Handlers (`redash/handlers/`) + +| Handler | File | Purpose | +| -------------- | ------------------- | --------------------------------- | +| Queries | `queries.py` | Query CRUD, execution, formatting | +| Dashboards | `dashboards.py` | Dashboard CRUD, sharing, widgets | +| Reports | `reports.py` | Turnilo report management | +| Visualizations | `visualizations.py` | Visualization types and configs | +| Data Sources | `data_sources.py` | Database connection management | +| Users | `users.py` | User management | +| Groups | `groups.py` | Group/permission management | +| Alerts | `alerts.py` | Query-based alerts | +| Model Configs | `model_configs.py` | OLAP data cube definitions | + +#### Data Models (`redash/models/`) + +| Model | Purpose | +| --------------- | ---------------------------- | +| `Query` | SQL query definitions | +| `QueryResult` | Cached query results | +| `Dashboard` | Dashboard containers | +| `Widget` | Dashboard widgets | +| `Visualization` | Visualization configurations | +| `DataSource` | Database connections | +| `User` | User accounts | +| `Group` | User groups with permissions | +| `Organization` | Multi-tenant organizations | +| `Report` | Turnilo-based OLAP reports | +| `Model` | OLAP data cube definitions | +| `Alert` | Query-based alerts | + +#### Query Runners (`redash/query_runner/`) + +Base classes: + +- `BaseQueryRunner` - Abstract base for all connectors +- `BaseSQLQueryRunner` - SQL-specific base class + +Key implementations: + +- `postgres.py` - PostgreSQL +- `mysql.py` - MySQL +- `big_query.py` - Google BigQuery +- `athena.py` - AWS Athena +- `druid.py` - Apache Druid +- `elasticsearch.py` - Elasticsearch + +#### Background Tasks (`redash/tasks/`) + +| Queue | Purpose | +| ------------------- | --------------------------------- | +| `periodic` | Scheduled tasks (5-min intervals) | +| `queries` | Query execution | +| `scheduled_queries` | Scheduled query runs | +| `emails` | Email notifications | +| `schemas` | Schema refresh | +| `default` | General operations | + +#### Plywood Bridge (`redash/plywood/`) + +| File | Purpose | +| ---------------------------- | ----------------------------------------------------- | +| `plywood.py` | `PlywoodApi` class - main interface to Plywood server | +| `hash_manager.py` | Hash serialization/deserialization for reports | +| `objects/data_cube.py` | OLAP DataCube model | +| `objects/expression.py` | Plywood expression objects | +| `parsers/query_parser_v2.py` | Query parsing with engine support | + +### Frontend (React) + +#### Key Components (`client/app/components/`) + +| Component | Purpose | +| --------------------- | ------------------------------------- | +| `TurniloComponent/` | OLAP exploration UI (Turnilo v1.40.5) | +| `visualizations/` | Visualization widget selector | +| `queries/` | Query editor components | +| `dashboards/` | Dashboard builder | +| `reports/` | Report management | +| `dynamic-parameters/` | Query parameter handling | + +#### Pages (`client/app/pages/`) + +| Page | Route | Purpose | +| --------------- | ----------------- | ----------------------- | +| `queries/` | `/queries/:id` | Query editor | +| `queries-list/` | `/queries` | Query browser | +| `dashboards/` | `/dashboards/:id` | Dashboard view/edit | +| `reports/` | `/reports` | Report browser | +| `report/` | `/reports/:id` | Report viewer (Turnilo) | +| `models/` | `/models` | Data cube management | +| `data-sources/` | `/data_sources` | Data source setup | + +### Plywood Server (TypeScript/Express) + +#### Endpoints (`plywood/src/endpoint/`) + +| Endpoint | Purpose | +| ------------------------------------ | --------------------------------------- | +| `/api/v1/plywood` | Main query translation | +| `/api/v1/plywood/attributes` | Extract dimensions/measures from schema | +| `/api/v1/plywood/attributes/engines` | List supported database engines | +| `/api/v1/plywood/expression` | Hash to expression conversion | +| `/api/v1/plywood/filter-to-hash` | Serialize filter to hash | +| `/api/v1/plywood/hash-to-filter` | Deserialize hash to filter | +| `/api/v1/plywood/response-shape` | Detect result data structure | + +#### Attribute Parsers (`plywood/src/formatter/attributesFormatter/parsers/`) + +Database-specific parsers that extract column metadata: + +- `PostgresAttributeParser.ts` +- `MySqlAttributeParser.ts` +- `BigQueryParser.ts` +- `AthenaParser.ts` +- `DruidParser.ts` +- `JsonAttributeParser.ts` + +--- + +## Data Flow + +### SQL Query Execution + +``` +1. Frontend submits query + POST /api/queries/{id}/results + +2. Backend handler (queries.py) + ├── Validate permissions + ├── Collect parameters + └── Enqueue to RQ + +3. RQ Worker executes + ├── Load DataSource + ├── Get QueryRunner (postgres, bigquery, etc.) + ├── Execute SQL + └── Store QueryResult + +4. Return to frontend + ├── Serialize results + └── Render visualization +``` + +### Turnilo Report Execution + +``` +1. TurniloComponent UI + └── User builds query (drag/drop dimensions) + +2. Plywood translation + ├── Send query state to Plywood server + ├── AttributeParser extracts dimensions/measures + └── Generate SQL hash + +3. Report creation + POST /api/reports { hash, model_id, ... } + +4. Report execution + ├── hash_to_result() in hash_manager.py + ├── PlywoodApi.convert_hash_to_expression() + ├── Translate hash → SQL + ├── Execute via QueryRunner + └── Return results to Turnilo UI +``` + +### Schema Discovery + +``` +1. Add data source + POST /api/data_sources + +2. Background refresh (every 30 min) + ├── For each DataSource + ├── QueryRunner.get_schema() + └── Store in DataSource.schema + +3. Auto-generate data cube (optional) + POST /api/model_configs/generate + ├── Call PlywoodApi.convert_attributes() + ├── Parse dimensions vs measures + └── Create Model (data cube) +``` + +### Database Engine Mapping + +Redash data source types map to Plywood engines: + +| Redash Type | Plywood Engine | +| ----------- | -------------- | +| `pg` | `postgres` | +| `mysql` | `mysql` | +| `bigquery` | `bigquery` | +| `athena` | `athena` | +| `druid` | `druid` | +| `json` | `json` | + +--- + +## API Reference + +### Main API (`/api/`) + +| Endpoint | Methods | Purpose | +| ------------------------------ | ---------------- | ---------------------- | +| `/api/queries` | GET, POST | List/create queries | +| `/api/queries/{id}` | GET, PUT, DELETE | Query CRUD | +| `/api/queries/{id}/results` | POST | Execute query | +| `/api/queries/format` | POST | Format SQL | +| `/api/dashboards` | GET, POST | List/create dashboards | +| `/api/dashboards/{id}` | GET, PUT, DELETE | Dashboard CRUD | +| `/api/dashboards/{id}/widgets` | POST | Add widget | +| `/api/reports` | GET, POST | List/create reports | +| `/api/reports/{id}` | GET, DELETE | Report CRUD | +| `/api/reports/{id}/results` | POST | Execute report (hash) | +| `/api/visualizations` | GET, POST | Visualization types | +| `/api/data_sources` | GET, POST | Data source management | +| `/api/model_configs` | GET, POST | Data cube configs | +| `/api/model_configs/generate` | POST | Auto-generate cubes | +| `/api/users` | GET, POST | User management | +| `/api/groups` | GET, POST | Group management | +| `/api/alerts` | GET, POST | Alert management | + +### Plywood API (`http://plywood:3000/api/v1/`) + +| Endpoint | Method | Purpose | +| ----------------------------- | ------ | --------------------------- | +| `/status` | GET | Health check | +| `/plywood` | POST | Query translation | +| `/plywood/attributes` | POST | Extract dimensions/measures | +| `/plywood/attributes/engines` | GET | List supported engines | +| `/plywood/expression` | POST | Hash to expression | +| `/plywood/filter-to-hash` | POST | Serialize filter | +| `/plywood/hash-to-filter` | POST | Deserialize filter | +| `/plywood/response-shape` | POST | Detect result schema | + +--- + +## Configuration + +### Environment Variables + +**Core:** + +```bash +REDASH_COOKIE_SECRET # Session encryption (REQUIRED) +REDASH_DATABASE_URL # PostgreSQL connection +REDASH_REDIS_URL # Redis for cache/sessions +RQ_REDIS_URL # Redis for job queue +``` + +**Plywood Integration:** + +```bash +PLYWOOD_SERVER_URL # Default: http://plywood-server:3000 +``` + +**AI/LLM:** + +```bash +OPENAI_API_KEY # OpenAI integration +GEMINI_API_KEY # Google Gemini +OLLAMA_API_URL # Default: http://ollama:11434 +``` + +**Email:** + +```bash +REDASH_MAIL_SERVER # SMTP server +REDASH_MAIL_PORT # SMTP port +REDASH_MAIL_USERNAME # SMTP credentials +REDASH_MAIL_PASSWORD +REDASH_MAIL_USE_TLS # TLS flag +``` + +**Security:** + +```bash +REDASH_ENFORCE_HTTPS # Redirect HTTP to HTTPS +REDASH_COOKIES_SECURE # Secure cookie flag +REDASH_AUTH_TYPE # Auth method +``` + +**Performance:** + +```bash +SQLALCHEMY_POOL_SIZE # DB connection pool +WORKERS_COUNT # Background workers +QUEUES # RQ queues to process +REDASH_SCHEMAS_REFRESH_SCHEDULE # Minutes between refresh +``` + +### Docker Services + +| Service | Port | Purpose | +| --------------- | ---------- | ------------ | +| `server` | 5000 | Flask API | +| `scheduler` | - | RQ scheduler | +| `worker-server` | 5001 | Worker HTTP | +| `redis` | 6379 | Cache/queue | +| `postgres` | 5432 | Database | +| `plywood` | 3000 | OLAP server | +| `email` | 1080, 1025 | Mail (dev) | + +--- + +## Development + +### Quick Start + +```bash +# Start all services +docker compose up --build + +# Initialize database +docker compose run server create_db + +# Create test database +docker compose run --rm postgres psql -h postgres -U postgres -c "create database tests" + +# Watch frontend +npm run watch +``` + +### Entry Points (`bin/docker-entrypoint`) + +| Command | Purpose | +| --------------- | ------------------------------- | +| `server` | Production Flask (gunicorn) | +| `dev_server` | Development Flask (auto-reload) | +| `debug` | Flask with debugger (PTVSD) | +| `worker` | RQ worker (supervisord) | +| `dev_worker` | Dev RQ worker (watch) | +| `dev_scheduler` | RQ scheduler | +| `worker_server` | Worker HTTP server | +| `create_db` | Initialize tables | +| `shell` | Python shell | +| `manage` | CLI commands | +| `tests` | Run pytest | + +### NPM Scripts + +```bash +npm run build # Production build +npm run watch # Watch mode +npm run test # Jest tests +npm run cypress # E2E tests +npm run lint # ESLint +npm run build:plywood # Build Plywood +npm run build:viz # Build viz-lib +``` + +### Makefile + +```bash +make up # Start services +make create_database # Init DB +make tests # Backend tests +make frontend-unit-tests # Frontend tests +make lint # Linting +make build # Production build +make clean # Clean Docker +``` + +--- + +## Key Files Reference + +### Backend Core + +| File | Purpose | +| ------------------------------- | ----------------------------- | +| `redash/__init__.py` | Global init (Redis, RQ, Mail) | +| `redash/app.py` | Flask app factory | +| `redash/wsgi.py` | Production entry | +| `redash/settings/__init__.py` | Configuration (400+ vars) | +| `redash/models/__init__.py` | ORM models (1,771 lines) | +| `redash/handlers/queries.py` | Query API | +| `redash/handlers/reports.py` | Report API | +| `redash/handlers/dashboards.py` | Dashboard API | + +### Plywood Integration + +| File | Purpose | +| ------------------------------------- | ------------------ | +| `redash/plywood/plywood.py` | PlywoodApi client | +| `redash/plywood/hash_manager.py` | Hash serialization | +| `redash/plywood/objects/data_cube.py` | OLAP model | + +### Frontend + +| File | Purpose | +| ----------------------------------------- | ------------- | +| `client/app/index.js` | React entry | +| `client/app/components/TurniloComponent/` | OLAP UI | +| `client/app/pages/queries/` | Query editor | +| `client/app/pages/reports/` | Report viewer | + +### Plywood Server + +| File | Purpose | +| -------------------------------------------- | -------------- | +| `plywood/src/app.ts` | Express app | +| `plywood/src/endpoint/` | API endpoints | +| `plywood/src/formatter/attributesFormatter/` | Column parsing | + +### Build & Config + +| File | Purpose | +| ----------------------- | ----------------- | +| `Dockerfile` | Multi-stage build | +| `docker-compose.yml` | Dev services | +| `pyproject.toml` | Python deps | +| `client/package.json` | Frontend deps | +| `bin/docker-entrypoint` | Container entry | + +--- + +## Authentication & Security + +### Auth Methods + +- **API Key** - Token-based for scripts +- **OAuth 2.0** - Google, GitHub +- **SAML 2.0** - Enterprise SSO +- **LDAP** - Directory services + +### Permission Levels + +| Level | Access | +| ----- | ---------------------- | +| View | Read-only | +| Edit | Modify objects | +| Admin | Full control + sharing | + +### Security Features + +- CSRF protection (Flask-WTF) +- Rate limiting (Flask-Limiter) +- CSP headers (Flask-Talisman) +- Secure cookies +- Encrypted credentials (FernetEngine) +- SSH tunneling (Paramiko) + +--- + +## Visualization Types + +### Built-in (viz-lib/) + +- Chart (line, bar, scatter, area) +- Table +- Heatmap +- Map (Leaflet) +- Funnel +- Gauge +- Number +- Pivot table +- Sankey +- Word cloud + +### Turnilo Visualizations + +- Pivot table with drill-down +- Detailed records +- Dimension/measure cross-tabs +- Time series with aggregations + +--- + +_Generated for development reference. Last updated: 2026-02-04_ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9c28e6bc..870ada542 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,22 @@ # Contributing Guide -Thank you for taking the time to contribute! :tada::+1: +Thank you for taking the time to contribute! :tada: :+1: -The following is a set of guidelines for contributing to Redash. These are guidelines, not rules, please use your best judgement and feel free to propose changes to this document in a pull request. +The following is a set of guidelines for contributing to DataReporter. These are guidelines, not rules, please use your best judgement and feel free to propose changes to this document in a pull request. -## Quick Links: +# Quick Links: -- [Feature Requests](https://discuss.redash.io/c/feature-requests) -- [Documentation](https://redash.io/help/) -- [Blog](https://blog.redash.io/) -- [Twitter](https://twitter.com/getredash) +- [Feature Requests](https://github.com/dataminelab/datareporter/discussions) +- [Documentation](https://datareporter.com/docs/) +- [GitHub Issues](https://github.com/dataminelab/datareporter/issues) --- + :star: If you already here and love the project, please make sure to press the Star button. :star: --- - -## Table of Contents +# Table of Contents [How can I contribute?](#how-can-i-contribute) @@ -32,24 +31,24 @@ The following is a set of guidelines for contributing to Redash. These are guide - [Release Method](#release-method) - [Code of Conduct](#code-of-conduct) -## How can I contribute? +# How can I contribute? -### Reporting Bugs +# Reporting Bugs When creating a new bug report, please make sure to: - Search for existing issues first. If you find a previous report of your issue, please update the existing issue with additional information instead of creating a new one. -- If you are not sure if your issue is really a bug or just some configuration/setup problem, please start a discussion in [the support forum](https://discuss.redash.io/c/support) first. Unless you can provide clear steps to reproduce, it's probably better to start with a thread in the forum and later to open an issue. +- If you are not sure if your issue is really a bug or just some configuration / setup problem, please start a discussion in [GitHub Discussions](https://github.com/dataminelab/datareporter/discussions) first. Unless you can provide clear steps to reproduce, it's probably better to start with a thread in the discussions and later to open an issue. - If you still decide to open an issue, please review the template and guidelines and include as much details as possible. -### Suggesting Enhancements / Feature Requests +# Suggesting Enhancements / Feature Requests If you would like to suggest an enhancement or ask for a new feature: -- Please check [the forum](https://discuss.redash.io/c/feature-requests/5) for existing threads about what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments. -- If there is no open thread, you're welcome to start one to have a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*. +- Please check [GitHub Discussions](https://github.com/dataminelab/datareporter/discussions) for existing threads about what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments. +- If there is no open thread, you're welcome to start one to have a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about _the problem you want to solve_ rather only _your proposed solution_. -### Pull Requests +# Pull Requests - **Code contributions are welcomed**. For big changes or significant features, it's usually better to reach out first and discuss what you want to implement and how (we recommend reading: [Pull Request First](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6#.ozlqxvj36)). This to make sure that what you want to implement is aligned with our goals for the project and that no one else is already working on it. - Include screenshots and animated GIFs in your pull request whenever possible. @@ -57,21 +56,17 @@ If you would like to suggest an enhancement or ask for a new feature: - Please follow existing code style: - Python: we use [Black](https://github.com/psf/black) to auto format the code. - Javascript: we use [Prettier](https://github.com/prettier/prettier) to auto-format the code. - -### Documentation - -The project's documentation can be found at [https://redash.io/help/](https://redash.io/help/). The [documentation sources](https://github.com/getredash/website/tree/master/src/pages/kb) are hosted on GitHub. To contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface. -## Additional Notes +# Documentation -### Release Method +The project's documentation can be found at [https://datareporter.com/docs/](https://datareporter.com/docs/). The [documentation sources](https://github.com/dataminelab/datareporter-docs) are hosted on GitHub. To contribute edits / new pages, open a pull request against the docs repository. -We publish a stable release every ~3-4 months, although the goal is to get to a stable release every month. +# Additional Notes -Every build of the master branch updates the *redash/redash:preview* Docker Image. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only. +# Release Method -When we release a new stable release, we also update the *latest* Docker image tag, the EC2 AMIs and GCE images. +We publish releases as Docker images. Every build of the main branch updates the Docker image. -## Code of Conduct +# Code of Conduct -This project adheres to the Contributor Covenant [code of conduct](https://redash.io/community/code_of_conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to team@redash.io. +This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to security@datareporter.com. diff --git a/Dockerfile b/Dockerfile index dd08e55af..76d4fb77f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,94 +1,120 @@ -FROM node:14.17 AS frontend-builder +FROM node:18-bookworm-slim AS frontend-builder # Controls whether to build the frontend assets ARG skip_frontend_build +ENV CYPRESS_INSTALL_BINARY=0 +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 + +RUN useradd -m -d /frontend datareporter +USER datareporter + WORKDIR /frontend -COPY bin/build_frontend.sh . -COPY client/ /frontend/client -COPY viz-lib/ /frontend/viz-lib -COPY plywood/server/ /frontend/plywood/server/ -RUN if [ "x$skip_frontend_build" = "x" ] ; then \ - echo "Building frontend";\ - ./build_frontend.sh;\ - else \ - echo "Skipping frontend build" &&\ - mkdir -p /frontend/client/dist &&\ - touch /frontend/client/dist/multi_org.html &&\ - touch /frontend/client/dist/index.html;\ - fi -FROM python:3.7-slim-buster +COPY --chown=datareporter client /frontend/client +COPY --chown=datareporter viz-lib /frontend/viz-lib +COPY --chown=datareporter plywood /frontend/plywood -EXPOSE 5000 +# Controls whether to instrument code for coverage information +ARG code_coverage +ENV BABEL_ENV=${code_coverage:+test} -# Controls whether to install extra dependencies needed for all data sources. -ARG skip_ds_deps -# Controls whether to install dev dependencies. -ARG skip_dev_deps +RUN < /etc/apt/sources.list.d/mssql-release.list && \ - apt-get update && \ -# ACCEPT_EULA=Y apt-get install -y msodbcsql17 && \ + apt-get install -y --no-install-recommends \ + pkg-config \ + curl \ + gnupg \ + build-essential \ + pwgen \ + libffi-dev \ + sudo \ + git-core \ + wget \ + # Kerberos, needed for MS SQL Python driver to compile on arm64 + libkrb5-dev \ + # OSError: mysql_config not found + libmariadb-dev \ + # Postgres client + libpq-dev \ + # ODBC support: + g++ unixodbc-dev \ + # for SAML + xmlsec1 \ + # Additional packages required for data sources: + libssl-dev \ + default-libmysqlclient-dev \ + freetds-dev \ + libsasl2-dev \ + unzip \ + python3-distutils \ + python3-venv \ + libsasl2-modules-gssapi-mit && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -#ARG databricks_odbc_driver_url=https://databricks.com/wp-content/uploads/2.6.10.1010-2/SimbaSparkODBC-2.6.10.1010-2-Debian-64bit.zip -#ADD $databricks_odbc_driver_url /tmp/simba_odbc.zip -#RUN unzip /tmp/simba_odbc.zip -d /tmp/ \ -# && dpkg -i /tmp/SimbaSparkODBC-*/*.deb \ -# && echo "[Simba]\nDriver = /opt/simba/spark/lib/64/libsparkodbc_sb64.so" >> /etc/odbcinst.ini \ -# && rm /tmp/simba_odbc.zip \ -# && rm -rf /tmp/SimbaSparkODBC* + +ARG TARGETPLATFORM +ARG databricks_odbc_driver_url=https://databricks-bi-artifacts.s3.us-east-2.amazonaws.com/simbaspark-drivers/odbc/2.6.26/SimbaSparkODBC-2.6.26.1045-Debian-64bit.zip +RUN < /etc/apt/sources.list.d/mssql-release.list + apt-get update + ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 + apt-get clean + rm -rf /var/lib/apt/lists/* + curl "$databricks_odbc_driver_url" --location --output /tmp/simba_odbc.zip + chmod 600 /tmp/simba_odbc.zip + unzip /tmp/simba_odbc.zip -d /tmp/simba + dpkg -i /tmp/simba/*.deb + printf "[Simba]\nDriver = /opt/simba/spark/lib/64/libsparkodbc_sb64.so" >> /etc/odbcinst.ini + rm /tmp/simba_odbc.zip + rm -rf /tmp/simba + fi +EOF WORKDIR /app -# Disalbe PIP Cache and Version Check -ENV PIP_DISABLE_PIP_VERSION_CHECK=1 -ENV PIP_NO_CACHE_DIR=1 - -# We first copy only the requirements file, to avoid rebuilding on every file -# change. -COPY requirements.txt requirements_bundles.txt requirements_dev.txt ./ -RUN if [ "x$skip_dev_deps" = "x" ] ; then pip install -r requirements.txt -r requirements_dev.txt; else echo "Skipping pip install dev dependencies" ; pip install -r requirements.txt; fi -COPY requirements_all_ds.txt ./ -RUN if [ "x$skip_ds_deps" = "x" ] ; then pip install -r requirements_all_ds.txt ; else echo "Skipping pip install -r requirements_all_ds.txt" ; fi - -COPY . /app -COPY --chown=redash --from=frontend-builder /frontend/client/dist /app/client/dist -RUN chown redash:redash -R /app -RUN find /app -USER redash +ENV POETRY_VERSION=2.1.1 +ENV POETRY_HOME=/etc/poetry +ENV POETRY_VIRTUALENVS_CREATE=false +RUN pip install --no-cache-dir "poetry==${POETRY_VERSION}" "distlib<0.4.0" + +# Avoid crashes, including corrupted cache artifacts, when building multi-platform images with GitHub Actions. +RUN poetry cache clear pypi --all + +COPY pyproject.toml poetry.lock ./ + +ARG POETRY_OPTIONS="--no-root --no-interaction --no-ansi" +# for LDAP authentication, install with `ldap3` group +# disabled by default due to GPL license conflict +ARG install_groups="main,all_ds,dev" +RUN poetry install --only $install_groups $POETRY_OPTIONS + +COPY --chown=datareporter . /app +COPY --chown=datareporter --from=frontend-builder /frontend/client/dist /app/client/dist +RUN chown datareporter:datareporter -R /app +USER datareporter + ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python ARG version ENV DATAREPORTER_VERSION=$version + ENTRYPOINT ["/app/bin/docker-entrypoint"] CMD ["server"] diff --git a/Jenkinsfile b/Jenkinsfile index 08c1fc24a..3b940ee22 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -95,7 +95,7 @@ node { stage("Build plywood-server docker image",) { echo "Build docker image for: ${appPlywoodServerName}" def imageNamePlywoodServer = "${registryRegion}/${appPlywoodServerName}:${latestTagRelease}-${shortCommit}" - dockerimagePlywoodServer = docker.build("${appPlywoodServerName}", "${imageLabel} ${buildArgs} ${noCache} plywood/server") + dockerimagePlywoodServer = docker.build("${appPlywoodServerName}", "${imageLabel} ${buildArgs} ${noCache} plywood") imageNames.add("${registryRegion}/${appPlywoodServerName}=" + imageNamePlywoodServer) } diff --git a/LICENSE b/LICENSE index 2b1e29818..6700abedb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,23 +1,199 @@ -Copyright (c) 2013-2020, Arik Fraimovich. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. + + Copyright 2024-2026 Radoslaw Maciaszek / dataminelab + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile index 7bc384919..f00ac3a21 100644 --- a/Makefile +++ b/Makefile @@ -1,57 +1,69 @@ -.PHONY: compose_build up test_db create_database clean down bundle tests lint backend-unit-tests frontend-unit-tests test build watch start redis-cli bash +.PHONY: compose_build up test_db create_database clean down tests lint fmt backend-unit-tests frontend-unit-tests test build watch start redis-cli bash compose_build: - docker-compose build + docker compose build up: - docker-compose up -d --build + docker compose up -d --build test_db: @for i in `seq 1 5`; do \ - if (docker-compose exec postgres sh -c 'psql -U postgres -c "select 1;"' 2>&1 > /dev/null) then break; \ + if (docker compose exec postgres sh -c 'psql -U postgres -c "select 1;"' 2>&1 > /dev/null) then break; \ else echo "postgres initializing..."; sleep 5; fi \ done - docker-compose exec postgres sh -c 'psql -U postgres -c "drop database if exists tests;" && psql -U postgres -c "create database tests;"' + docker compose exec postgres sh -c 'psql -U postgres -c "drop database if exists tests;" && psql -U postgres -c "create database tests;"' + +pull-deepseek-r1: + docker compose exec ollama ollama pull deepseek-r1:7b create_database: - docker-compose run server create_db + docker compose run server create_db clean: - docker-compose down && docker-compose rm + docker compose down && docker compose rm down: - docker-compose down - -bundle: - docker-compose run server bin/bundle-extensions + docker compose down tests: - docker-compose run server tests + docker compose run server tests + +fmt: + @echo "Formatting changed files..." + @CHANGED=$$(git diff --cached --name-only --diff-filter=ACM; git diff --name-only --diff-filter=ACM); \ + CHANGED=$$(echo "$$CHANGED" | sort -u); \ + JS_FILES=$$(echo "$$CHANGED" | grep -E '\.(js|jsx|ts|tsx|json|css|scss|less|md)$$'); \ + PY_FILES=$$(echo "$$CHANGED" | grep -E '\.py$$'); \ + if [ -z "$$CHANGED" ]; then echo "No changed files to format."; exit 0; fi; \ + if [ -n "$$JS_FILES" ]; then echo "$$JS_FILES" | xargs npx prettier --config .prettierrc.json --write; fi; \ + if [ -n "$$PY_FILES" ]; then echo "$$PY_FILES" | xargs black; echo "$$PY_FILES" | xargs ruff check --fix 2>/dev/null; fi; \ + STAGED=$$(git diff --cached --name-only --diff-filter=ACM); \ + if [ -n "$$STAGED" ]; then echo "$$STAGED" | xargs git add; fi + @echo "Done. Changed files formatted." lint: - ./bin/flake8_tests.sh + flake8 --config=.flake8 . backend-unit-tests: up test_db - docker-compose run --rm --name tests server tests + docker compose run --rm --name tests server tests -frontend-unit-tests: bundle +frontend-unit-tests: npm ci - npm run bundle npm test test: lint backend-unit-tests frontend-unit-tests -build: bundle +build: npm run build -watch: bundle +watch: npm run watch -start: bundle +start: npm run start redis-cli: - docker-compose run --rm redis redis-cli -h redis + docker compose run --rm redis redis-cli -h redis bash: - docker-compose run --rm server bash + docker compose run --rm server bash diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..bdeef7571 --- /dev/null +++ b/NOTICE @@ -0,0 +1,74 @@ +DataReporter +Copyright (c) 2024-2026 Radoslaw Maciaszek / dataminelab +https://datareporter.com + +This product is licensed under the Apache License, Version 2.0. + +This product includes software developed by third parties: + +========================================================================= +Redash +Copyright (c) 2013-2020, Arik Fraimovich +Licensed under the BSD 2-Clause "Simplified" License +https://github.com/getredash/redash +========================================================================= + +========================================================================= +Turnilo +Copyright (c) 2017-2019 Allegro.pl +Licensed under the Apache License, Version 2.0 +https://github.com/allegro/turnilo + +Turnilo includes software originally developed by: + Imply Data, Inc. (2015-2016) - originally released as "Pivot" + Licensed under the Apache License, Version 2.0 + +The Turnilo component included in this product is a hard fork of +Turnilo v1.40.5 (forked February 2025) with modifications by +dataminelab for integration into DataReporter. +========================================================================= + +========================================================================= +Plywood +Copyright (c) Imply Data, Inc. +Licensed under the Apache License, Version 2.0 +https://github.com/implydata/plywood + +The reporter-plywood client is a fork of the Plywood library with +modifications by dataminelab for DataReporter integration. +========================================================================= + +========================================================================= +Roboto Font +Copyright (c) Google +Licensed under the Apache License, Version 2.0 +https://fonts.google.com/specimen/Roboto +========================================================================= + +========================================================================= +Open Sans Font +Copyright (c) Google +Licensed under the Apache License, Version 2.0 +https://fonts.google.com/specimen/Open+Sans +(Used in TurniloComponent) +========================================================================= + +========================================================================= +Plywood (original) +Copyright (c) 2015 Metamarkets Group Inc. +Licensed under the Apache License, Version 2.0 +(Portions of plywood/client derive from the original facetjs library) +========================================================================= + +========================================================================= +drag-drop-polyfill +Copyright (c) 2016 Tim Ruffles +Licensed under the BSD 2-Clause License +(Used in TurniloComponent) +========================================================================= + +========================================================================= +query_order.py +Copyright (c) 2012, Konsta Vesterinen +Licensed under the BSD 3-Clause License +========================================================================= diff --git a/README.md b/README.md index 854d78d93..7c32d1471 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,143 @@

- + DataReporter

-[![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/) +

DataReporter

+

Ask your data anything.

-Data Reporter is a business intelligence, data exploration and visualization web application. -Harness the power of data big and small and explore, query, visualize, and share data from SQL data sources. +

+ Open-source BI with AI. Ask questions in plain English, drag & drop dashboards, or write SQL.
+ One platform, three ways to explore — your data never leaves your infrastructure. +

+ +

+ Website · + Quick Start · + Discussions +

-DataReporter is a fork of Turnilo which is currently available under Apache 2.0 license. DataReporter is also a fork of Redash, which is currently available under BSD-2-Clause license. DataReporter core is released under Apache 2.0 license. +

+ License + GitHub Stars + Last Commit +

-DataReporter manifesto: +--- -* High usability for non-technical users over sophisticated but rarely used features. -* Self-describing reports for users without deep domain expertise. -* Data cubes configuration as a code. -* Focus on the "big data" cloud databases -* Browser-based: Everything in your browser, with a shareable URL. -* Ease-of-use: Become immediately productive with data without the need to master complex software. -4. **Visualization and dashboards**: Create [beautiful visualizations](https://redash.io/help/user-guide/visualizations/visualization-types) with drag and drop, and combine them into a single dashboard. -5. **Sharing**: Collaborate easily by sharing visualizations and their associated queries, enabling peer review of reports and queries. -6. **Schedule refreshes**: Automatically update your charts and dashboards at regular intervals you define. -8. **REST API**: Everything that can be done in the UI is also available through REST API. + -## Getting Started +## Quick Start -* [Setting up DataReporter instance](SETUP.md) -* [Documentation](TBD). +Get running in under 5 minutes: -## Supported Data Sources +```bash +git clone https://github.com/dataminelab/datareporter.git +cd datareporter +cp .env.example .env # Add your AI key (OpenAI, Gemini, or use local Ollama) +docker compose up --build +docker compose run --rm server create_db +``` + +Open [http://localhost:5000](http://localhost:5000) and start asking questions. + +## Who Is This For? -DataReporter supports initially 5 SQL. It can also be extended to support more. Below is a list of built-in sources: +- **Business users** who want answers from data without learning SQL or waiting for the data team +- **Data analysts** who want drag-and-drop OLAP exploration alongside a full SQL editor +- **Engineering teams** who need a self-hosted BI platform with API access and config-as-code +- **Privacy-conscious organizations** that want AI-powered analytics without sending data to third parties +- **Small companies** that need Looker/Tableau-level capabilities on an open-source budget -- Amazon Athena -- Druid -- Google BigQuery -- MySQL -- PostgreSQL +## What Makes DataReporter Different? -## Getting Help +**1. AI-native from the ground up.** Ask "What were our top products last quarter?" in plain English. DataReporter's AI reads your database schema, generates SQL, executes it, and returns an interactive chart. Choose between GPT, Gemini, DeepSeek, or run locally with Ollama — your data stays on your infrastructure. -* Issues: https://github.com/dataminelab/datareporter/issues -* Discussion Forum: TBD +**2. Three exploration modes, one platform.** Most BI tools force you into one paradigm. DataReporter gives you natural language (AI), drag & drop (OLAP), and SQL — all producing the same shareable dashboards. -## Reporting Bugs and Contributing Code +**3. Auto-generated data cubes.** Connect a database, and DataReporter automatically discovers your schema and generates OLAP cubes for drag-and-drop exploration. No manual configuration needed. -* Want to report a bug or request a feature? Please open [an issue](https://github.com/dataminelab/datareporter/issues/new). -* Want to help us build **_Redash_**? Fork the project, edit in a develop branch and make a pull request. We need all the help we can get! +## Features + +**AI Engine** + +- Natural language to SQL — ask questions, get charts +- Multi-provider: GPT, Gemini, DeepSeek, Ollama (local/private) +- Conversational follow-ups that refine your analysis +- AI generates SQL you can inspect, edit, and save + +**Visualization** + +- 14+ chart types: line, bar, scatter, area, heatmap, map, funnel, gauge, pivot table, sankey, word cloud, and more +- Drag-and-drop dashboard builder +- Scheduled refreshes and alerts +- Shareable URLs for every dashboard and report + +**Enterprise Ready** + +- Authentication: OAuth 2.0, SAML 2.0, LDAP, API keys +- Role-based access control with groups and organizations +- SSH tunneling to databases behind firewalls +- CSRF protection, rate limiting, CSP headers, encrypted credentials +- Deploy with Docker, Kubernetes, or Cloud Run + +## Supported Data Sources + +**60+ connectors** out of the box. Full drag & drop OLAP support for: + +| Data Source | SQL | AI Queries | Drag & Drop | +| --------------- | :-: | :--------: | :---------: | +| Google BigQuery | Yes | Yes | Yes | +| Amazon Athena | Yes | Yes | Yes | +| Apache Druid | Yes | Yes | Yes | +| PostgreSQL | Yes | Yes | Yes | +| MySQL | Yes | Yes | Yes | + +Plus Snowflake, ClickHouse, Databricks, Trino, Presto, MS SQL, Oracle, Redshift, MongoDB, Elasticsearch, Cassandra, SQLite, and [40+ more](ARCHITECTURE.md#data-source-drivers). + +## Architecture + +``` + ┌─────────────────────────┐ + │ Your Question │ + │ "Show me top products" │ + └────────────┬────────────┘ + │ + ┌──────────────────┼──────────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ AI Chat │ │ Drag & │ │ SQL │ + │ (NLQ) │ │ Drop │ │ Editor │ + └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────┐ + │ 60+ Database Connectors │ + └─────────────────────────────────────────────┘ + │ + ▼ + ┌──────────┐ + │ Charts, │ + │ Tables, │ + │ Dashboards│ + └──────────┘ +``` + +See [ARCHITECTURE.md](ARCHITECTURE.md) for the full technical overview including API reference. + +## Contributing + +- **Report a bug** — [Open an issue](https://github.com/dataminelab/datareporter/issues/new) +- **Suggest a feature** — [Start a discussion](https://github.com/dataminelab/datareporter/discussions) +- **Contribute code** — See [CONTRIBUTING.md](CONTRIBUTING.md) | Development setup: [SETUP.md](SETUP.md) ## Security -Please email security@datareporter.com to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. +Email [security@datareporter.com](mailto:security@datareporter.com) to report vulnerabilities. See [SECURITY.md](SECURITY.md). ## License -Apache-2.0-License. +[Apache License 2.0](LICENSE) + +DataReporter includes code from [Redash](https://github.com/getredash/redash) (BSD-2-Clause) and [Turnilo](https://github.com/allegro/turnilo) (Apache-2.0). See [NOTICE](NOTICE) for full attribution. diff --git a/SECURITY.md b/SECURITY.md index 2bfe8d534..9109cc0cc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,4 +2,4 @@ ## Reporting a Vulnerability -Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use [this PGP key](https://keybase.io/arikfr/key.asc). +Please email [security@datareporter.com](mailto:security@datareporter.com) to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use [this PGP key](https://keybase.io/arikfr/key.asc). diff --git a/SETUP.md b/SETUP.md index e51fc9285..40eb8d4c7 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,155 +1,313 @@ -## Dev environment +# Dev Environment -DataReporter +Setup guide for DataReporter's development environment. -Requirements: -* DataReporter builds correctly with Node 12 +## Prerequisites -Consider using [nodenv](https://joshmorel.ca/post/node-virtual-environments-with-nodenv/) +### Node.js 18.20 -Requirements: -* Data reported builds correctly with Node 12 -* Install node 12.22.12 with nodenv and ensure shims are added to PATH -see for more info: https://github.com/nodenv/nodenv#how-it-works -see https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-wsl for windows-wsl2-nvm +DataReporter builds correctly with Node version 18.20. Use [nodenv](https://joshmorel.ca/post/node-virtual-environments-with-nodenv/) or nvm: +- [Ensure shims are added to PATH](https://github.com/nodenv/nodenv#how-it-works) +- [For Windows WSL2 with nvm](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-wsl) + +**Using nodenv:** + +```sh +nodenv install 18.20 +nodenv local 18.20 +``` + +**Using nvm:** + +```sh +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash +nvm install v18.20 +nvm alias default v18.20 ``` -nodenv install 12.22.12 -nodenv local 12.22.12 + +To auto-select the Node version on new terminals, add to `.bashrc` or `.bash_profile`: + +```sh +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +nvm use v18.20 > /dev/null ``` -* Build UI - Required to build ui for - * Enter project root directory - * `cd client` - * `npm install` Installs all node dependencies to for redash - * `npm run build` Builds front end to the folder `client/dist/` +### Python 3.10 and dependencies + +```sh +sudo apt install -y python3.10 python3.10-venv python3.10-dev +sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 +python3.10 --version +curl -sSL https://install.python-poetry.org | POETRY_VERSION=2.1.1 python3 - +poetry --version +POETRY_OPTIONS="--no-root --no-interaction --no-ansi" +install_groups="main,all_ds,dev" +poetry install --only $install_groups $POETRY_OPTIONS +``` -* Build Plywood - * Enter project root directory - * `cd plywood/server` - * `npm install` Installs all node dependencies to for plywood - * `npm run build` Builds plywood server end to the folder `plywood/server/dist/` +## Environment Setup -* Setup docker compose - * `make up` or `docker-compose up --build` to start required services like postgres app server - * `docker-compose run --rm server create_db` Will start server and run. exec /app/manage.py database create_tables. - This step is required **only once**. - * Any change to SQL data made on python side requires to create a migration file for upgrading the required database columns: `docker-compose run server manage db migrate` - * Later on and only if necessary, in order to upgrade local database run: `docker-compose run --rm server manage db upgrade` +Set up environment variables before starting Docker services. Copy the example file and adjust values for your local setup: +```sh +cp .env.example .env +# Edit .env to set your configuration +``` -* Not needed anymore, might be useful for local development: start UI proxy - * Enter project root directory - * `cd client` - * `npm run start` Starts babel and webpack dev server which will proxy redash and plywood backend +For reference, see `.env.example` in the project root for sample variables and expected formats. -* `open http://localhost:5000` +## Git Hooks -## Local Development +The repo includes shared git hooks in `.githooks/`. To enable them, point git at that directory: -Consider using [pyenv](https://github.com/pyenv/pyenv#installation) for installing local Python pyenv app. Datareporter container images are shipped with Python 3.8.7, [ubuntu guide](https://www.dedicatedcore.com/blog/install-pyenv-ubuntu/) +```sh +git config core.hooksPath .githooks ``` -# install necessary python version -pyenv install 3.8.7 -# make sure you run below command in the datareported folder -# automatically select whenever you are in the current directory (or its subdirectories) -pyenv local 3.8.7 -# note that on certani linux distros you might need to also run below command -# $ git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv -# create virtualenv -pyenv virtualenv 3.8.7 .venv -source ./.venv/bin/activate -# note that in some system .venv might be created in your home folder: /.pyenv/versions/.venv -# $ source ~/.pyenv/versions/.venv/bin/activate + +This is a one-time setup per clone. The setting is stored in your local `.git/config` and doesn't affect other repos. + +**What the hooks do:** + +The pre-commit hook **blocks the commit** if any check fails. It checks: + +| Check | Tool | Fix command | +| ------------------------------- | -------- | ------------------------------------------------------- | +| JS/TS/CSS/JSON/MD formatting | prettier | `npx prettier --config .prettierrc.json --write ` | +| Python formatting | black | `black ` | +| Python linting + import sorting | ruff | `ruff check --fix ` | + +Tools that aren't installed are skipped silently. + +### VS Code / GUI clients + +The hook works with VS Code's commit button and other GUI git clients — they respect `core.hooksPath`. If a co-worker reports the hook isn't running, check: + +1. **Did they run the setup command?** `git config core.hooksPath .githooks` must be run once per clone. Verify with: `git config --get core.hooksPath` (should print `.githooks`). +2. **Check the error output.** VS Code shows hook output in the Git Output panel (`View → Output → Git`). If the commit is blocked, fix the listed files and re-commit. To bypass in emergencies: `git commit --no-verify`. + +> **Note:** `core.hooksPath` replaces `.git/hooks/` entirely. If you have personal hooks there, move them to `.githooks/` instead. + +## Docker Compose Setup + +Start the backend services (postgres, redis, server, plywood): + +```sh +docker compose up --build ``` -Installation in Linux using virtualenvwrapper: +Initialize the database (first time only): + +```sh +docker compose run --rm server create_db ``` -sudo pacman -S yay -yay -S python38 -mkvirtualenv -p /usr/bin/python3.8 python38 + +Database migration commands: + +```sh +# If you get "target database is not up to date": +docker compose run server manage db stamp head +# Create migration after backend model changes: +docker compose run server manage db migrate +# Apply pending migrations: +docker compose run --rm server manage db upgrade ``` -If working with Visual Studio Code +## Frontend Development + +There are two ways to develop the frontend. **Option 1 is recommended** for day-to-day work. -Follow the [tutorial](https://redash.io/help/open-source/dev-guide/debugging) +### Option 1: Hot-reload with webpack-dev-server (recommended) -And run the debugging session: +Runs a local dev server with hot module replacement. Changes appear instantly in the browser without rebuilding. + +**Terminal 1 — Backend (Docker):** + +```sh +docker compose up ``` -# install below library -pip install ptvsd -# start debugging session using below line -docker-compose stop server && docker-compose run --rm --service-ports server debug && docker-compose start server +**Terminal 2 — Frontend (host):** + +```sh +cd client +npm install # first time only +npm run start ``` -### Running tests locally +Open **`http://localhost:8080`** in your browser. The webpack-dev-server proxies API calls (`/api`, `/login`, `/plywood`, etc.) to the Docker backend at `localhost:5000` and plywood at `localhost:3000`. Edit client code, save, and see changes immediately. + +| Command | What it does | +| --------------- | ------------------------------------------------------------------------------------ | +| `npm run start` | webpack-dev-server + viz-lib watcher — hot reload at port 8080 | +| `npm run watch` | Rebuilds `client/dist/` on file change — Docker serves updates at port 5000 (slower) | +| `npm run dev` | Same as `start` with `--openssl-legacy-provider` for older Node compatibility | + +### Linux: file watcher limit + +On Linux you may hit the inotify watcher limit: -First ensure that the "tests" database is created: ``` -docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests" +Error: ENOSPC: System limit for number of file watchers reached, watch ``` -Then run the tests: -``` -docker-compose run --rm server tests +Fix: + +```sh +sudo sysctl -w fs.inotify.max_user_watches=512000 ``` +## Architecture + +### Ports + +| Service | Port | Purpose | +| ------------------ | ----------- | ----------------------------------------------------- | +| webpack-dev-server | 8080 | Frontend dev with hot reload (host only, not Docker) | +| server | 5000 | Python backend API + serves production `client/dist/` | +| plywood | 3000 | Plywood/Turnilo OLAP server | +| postgres | 5432, 15432 | Database | +| redis | 6379 | Cache and job queue | +| email (maildev) | 1080, 1025 | Local email testing UI and SMTP | +| server debug | 5678 | Python debugger | +| plywood debug | 9231 | Node.js debugger | + ### Components -#### Datareporter server -* **directory**: `redash` -* **debug**: Please follow the instruction from [redash](https://redash.io/help/open-source/dev-guide/debugging) -* **changes:** - * All changes are immediately visible as the python application is interpreted and it's running directly from source code. -#### Datareporter frontend - * **submodules** - for debug and changes they follow root fronted app: - * Lib viz - * **directory:** `viz-lib` - * Plywood client - * **directory:** `plywood/client` - * **directory:** `client` - * **debug:** Can be debugged from browser open application at `http://localhost:8080` || `5000` and use browser debugger. - * **changes:** - * By default, changes are not reflected. You need go into `client` directory and start `npm run watch`. - That will start watched for source code changes for Datareporter frontend and all submodules. - * At liniux system you may face problem of too many file system watchers. That will result in error message - ```Error: ENOSPC: System limit for number of file watchers reached, watch ``` - To solve it you need to increase the number of available watches by : - ```sudo sysctl -w fs.inotify.max_user_watches=512000``` - -#### Plywood server -* **directory:** `plywood/server` -* **debug:** connect nodejs debugger to `localhost:9231` -* **changes:** - * All changes should be reflected automatically. The server is running in watch mode with incremental build support - and should rebuild at any source code change. - * To see details/logs of build go into repo root dir and run `docker-compose logs plywood` - -### Publishing NPM reporter-plywood package -This is depricated but still available for backward compatibility. -First make sure to authenticate with `npm login` then build and publish the package: - -``` -cd plywood/client -npm install -npm run compile -npm publish -``` -### Debugging notes +#### Backend (Python) + +- **Directory:** `redash` +- **Debug:** Follow the [debugging guide](/docs/open-source/dev-guide/debugging/) +- **Changes:** Immediately visible — Python runs directly from source via the bind mount. + +#### Frontend (JavaScript) + +- **Directories:** `client`, `viz-lib`, `plywood/client` +- **Debug:** Open `http://localhost:8080` (dev server) or `http://localhost:5000` (Docker) and use browser devtools. +- **Changes:** Use `npm run start` in `client/` for hot reload, or `npm run watch` to rebuild `dist/` on change. + +#### Plywood server (Node.js) + +- **Directory:** `plywood` +- **Debug:** Connect Node.js debugger to `localhost:9231` +- **Changes:** Automatically reflected — runs in watch mode with incremental builds. +- **Logs:** `docker compose logs plywood` + +### Supported Report Engines + +postgres, mysql, bigquery, athena, druid, pg, json + +## Testing + +### Backend + +```sh +# Create test database (first time only): +docker compose run --rm postgres psql -h postgres -U postgres -c "create database tests" +# Run all tests: +docker compose run --rm server tests + +# Run tests for a specific module: +docker compose run --rm server pytest -v tests/plywood/test_json.py +``` + +### viz-lib + +```sh +cd viz-lib +npm run test +``` + +### End-to-end (Cypress) + +```sh cd client -npm start +npm run cypress db-seed # Seed database with test data +npm run cypress run # Run Cypress tests in headless mode +``` + +## Debugging + +### Python backend (VS Code) + +Follow the [debugging guide](/docs/open-source/dev-guide/debugging/), then: + +```sh +pip install ptvsd +docker compose stop server && docker compose run --rm --service-ports server debug && docker compose start server +``` + +### Plywood logs + +Set log mode in `docker-compose.yml` environment or override: + +- `LOG_MODE=request_and_response` — full request/response logging +- `LOG_MODE=response_only` — responses only + +### Ollama (local AI) + +If using the Ollama service for local AI, download the model first: + +```sh +docker compose exec ollama ollama pull deepseek-r1:7b +``` + +### Alternative Docker Compose config + +To use the dev-specific compose override: + +```sh +docker compose -f compose.dev.yml up -d +``` + +## Troubleshooting -visit http://localhost:8080/ instead of using port 5050 +### Docker build issues -To run Python debugger: -docker-compose stop server && docker-compose run --rm --service-ports server debug && docker-compose start server +If you have issues building Docker images, try removing the Docker config: -To log messages to/from Plywood add to the Plywood env (in docker-compose) following variable: `LOG_MODE=request_and_response` or `LOG_MODE=response_only` +```bash +rm ~/.docker/config.json +``` + +### Docker container networking -### Docker installation issues +Useful when testing connections between containers (e.g., connecting to a router container from DataReporter): -if you are having issue building docker images, try to remove ~/.docker/config.json file ```bash -rm ~/.docker/config.json -``` \ No newline at end of file +docker network connect datareporter_default router +docker inspect -f '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}' router +docker inspect -f '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}' datareporter-server-1 +docker exec datareporter-server-1 ping router -c2 +``` + +## Reference + +### Python environment with pyenv + +For local Python development outside Docker: + +```sh +pyenv install 3.10 +pyenv local 3.10 +pyenv virtualenv 3.10 .venv +source ./.venv/bin/activate +``` + +See [pyenv installation](https://github.com/pyenv/pyenv#installation) for setup instructions. + +### Python package management (Poetry) + +Backend uses [Poetry](https://python-poetry.org/) for dependency management: + +```sh +# Install poetry +pip3 install poetry==2.1.1 + +# Add a new package +poetry add + +# Remove a package +poetry remove +``` diff --git a/bin/build_frontend.sh b/bin/build_frontend.sh deleted file mode 100755 index d47a979b6..000000000 --- a/bin/build_frontend.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -cd client -echo "Clean install viz-lib & redash-client " -npm ci --unsafe-perm || cat /root/.npm/_logs/* -echo "Build viz-lib & redash-client " -npm run build || cat /root/.npm/_logs/* diff --git a/bin/bundle-extensions b/bin/bundle-extensions deleted file mode 100755 index fc7814f18..000000000 --- a/bin/bundle-extensions +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Copy bundle extension files to the client/app/extension directory""" -import logging -import os -from pathlib import Path -from shutil import copy -from collections import OrderedDict as odict - -from importlib_metadata import entry_points -import importlib_resources - -# Name of the subdirectory -BUNDLE_DIRECTORY = "bundle" - -logger = logging.getLogger(__name__) - - -# Make a directory for extensions and set it as an environment variable -# to be picked up by webpack. -extensions_relative_path = Path("client", "app", "extensions") -extensions_directory = Path(__file__).parent.parent / extensions_relative_path - -if not extensions_directory.exists(): - extensions_directory.mkdir() -os.environ["EXTENSIONS_DIRECTORY"] = str(extensions_relative_path) - - -def entry_point_module(entry_point): - """Returns the dotted module path for the given entry point""" - return entry_point.pattern.match(entry_point.value).group("module") - - -def load_bundles(): - """"Load bundles as defined in Redash extensions. - - The bundle entry point can be defined as a dotted path to a module - or a callable, but it won't be called but just used as a means - to find the files under its file system path. - - The name of the directory it looks for files in is "bundle". - - So a Python package with an extension bundle could look like this:: - - my_extensions/ - ├── __init__.py - └── wide_footer - ├── __init__.py - └── bundle - ├── extension.js - └── styles.css - - and would then need to register the bundle with an entry point - under the "redash.bundles" group, e.g. in your setup.py:: - - setup( - # ... - entry_points={ - "redash.bundles": [ - "wide_footer = my_extensions.wide_footer", - ] - # ... - }, - # ... - ) - - """ - bundles = odict() - for entry_point in entry_points().select(group="redash.bundles"): - logger.info('Loading Redash bundle "%s".', entry_point.name) - module = entry_point_module(entry_point) - # Try to get a list of bundle files - try: - bundle_dir = importlib_resources.files(module).joinpath(BUNDLE_DIRECTORY) - except (ImportError, TypeError): - # Module isn't a package, so can't have a subdirectory/-package - logger.error( - 'Redash bundle module "%s" could not be imported: "%s"', - entry_point.name, - module, - ) - continue - if not bundle_dir.is_dir(): - logger.error( - 'Redash bundle directory "%s" could not be found or is not a directory: "%s"', - entry_point.name, - bundle_dir, - ) - continue - bundles[entry_point.name] = list(bundle_dir.rglob("*")) - return bundles - - -bundles = load_bundles().items() -if bundles: - print("Number of extension bundles found: {}".format(len(bundles))) -else: - print("No extension bundles found.") - -for bundle_name, paths in bundles: - # Shortcut in case not paths were found for the bundle - if not paths: - print('No paths found for bundle "{}".'.format(bundle_name)) - continue - - # The destination for the bundle files with the entry point name as the subdirectory - destination = Path(extensions_directory, bundle_name) - if not destination.exists(): - destination.mkdir() - - # Copy the bundle directory from the module to its destination. - print('Copying "{}" bundle to {}:'.format(bundle_name, destination.resolve())) - for src_path in paths: - dest_path = destination / src_path.name - print(" - {} -> {}".format(src_path, dest_path)) - copy(str(src_path), str(dest_path)) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 2bd3aca65..bc069b05a 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -12,12 +12,14 @@ dev_scheduler() { exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- ./manage.py rq scheduler } + ensure_schema() { if [[ "$DATAREPORTER_ENSURE_SCHEMA" == "true" ]]; then echo "starting schema creation" /app/bin/ensure_schema.sh & fi } + worker() { ensure_schema @@ -29,6 +31,7 @@ worker() { supervisord -c worker.conf } + worker_server() { ensure_schema @@ -40,6 +43,20 @@ worker_server() { REDASH_WEB_WORKER_THREADS=${REDASH_WEB_WORKER_THREADS-8} exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name worker -w${REDASH_WEB_WORKERS:-4} --threads ${REDASH_WEB_WORKER_THREADS} --timeout $REDASH_WEB_WORKER_TIMOUT worker.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER } + +workers_healthcheck() { + WORKERS_COUNT=${WORKERS_COUNT} + echo "Checking active workers count against $WORKERS_COUNT..." + ACTIVE_WORKERS_COUNT=`echo $(rq info --url $REDASH_REDIS_URL -R | grep workers | grep -oP ^[0-9]+)` + if [ "$ACTIVE_WORKERS_COUNT" -lt "$WORKERS_COUNT" ]; then + echo "$ACTIVE_WORKERS_COUNT workers are active, Exiting" + exit 1 + else + echo "$ACTIVE_WORKERS_COUNT workers are active" + exit 0 + fi +} + dev_worker() { echo "Starting dev RQ worker..." @@ -73,6 +90,7 @@ help() { echo "dev_worker -- start a single RQ worker with code reloading" echo "scheduler -- start an rq-scheduler instance" echo "dev_scheduler -- start an rq-scheduler instance with code reloading" + echo "workers_healthcheck -- start a healthcheck operation for workers" echo "" echo "shell -- open shell" echo "dev_server -- start Flask development server with debugger and auto reload" @@ -93,12 +111,7 @@ tests() { exec pytest $TEST_ARGS } -case "$1" in -worker) - shift - worker - ;; -dev_worker_server) +dev_worker_server() { echo "Starting dev worker server" export FLASK_DEBUG=1 # Recycle gunicorn workers every n-th request. See http://docs.gunicorn.org/en/stable/settings.html#max-requests for more details. @@ -107,12 +120,26 @@ dev_worker_server) REDASH_WEB_WORKERS=1 REDASH_WEB_WORKER_TIMOUT=180 REDASH_WEB_WORKER_THREADS=1 - exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name worker -w${REDASH_WEB_WORKERS:-4} --threads ${REDASH_WEB_WORKER_THREADS} --timeout $REDASH_WEB_WORKER_TIMOUT worker.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER + exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- python -m debugpy --listen 0.0.0.0:5678 /app/dev_worker_server.py runserver --debugger --no-reload -h 0.0.0.0 --without-threads +} + +case "$1" in +worker) + shift + worker + ;; +dev_worker_server) + shift + dev_worker_server ;; worker_server) shift worker_server ;; +workers_healthcheck) + shift + workers_healthcheck + ;; server) shift server diff --git a/bin/flake8_tests.sh b/bin/flake8_tests.sh deleted file mode 100755 index 3c27f7fee..000000000 --- a/bin/flake8_tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -o errexit # fail the build if any task fails - -flake8 --version ; pip --version -# stop the build if there are Python syntax errors or undefined names -flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics -# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide -flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics diff --git a/bin/get_changes.py b/bin/get_changes.py index 60091bb77..aad122383 100644 --- a/bin/get_changes.py +++ b/bin/get_changes.py @@ -1,35 +1,44 @@ #!/bin/env python3 -import sys import re import subprocess +import sys def get_change_log(previous_sha): - args = ['git', '--no-pager', 'log', '--merges', '--grep', 'Merge pull request', '--pretty=format:"%h|%s|%b|%p"', 'master...{}'.format(previous_sha)] + args = [ + "git", + "--no-pager", + "log", + "--merges", + "--grep", + "Merge pull request", + '--pretty=format:"%h|%s|%b|%p"', + "master...{}".format(previous_sha), + ] log = subprocess.check_output(args) changes = [] - for line in log.split('\n'): + for line in log.split("\n"): try: - sha, subject, body, parents = line[1:-1].split('|') + sha, subject, body, parents = line[1:-1].split("|") except ValueError: continue try: - pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0] + pull_request = re.match(r"Merge pull request #(\d+)", subject).groups()[0] pull_request = " #{}".format(pull_request) - except Exception as ex: + except Exception: pull_request = "" - author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1] + author = subprocess.check_output(["git", "log", "-1", '--pretty=format:"%an"', parents.split(" ")[-1]])[1:-1] changes.append("{}{}: {} ({})".format(sha, pull_request, body.strip(), author)) return changes -if __name__ == '__main__': +if __name__ == "__main__": previous_sha = sys.argv[1] changes = get_change_log(previous_sha) diff --git a/bin/release_manager.py b/bin/release_manager.py index 3d9b21c89..bd2200d52 100644 --- a/bin/release_manager.py +++ b/bin/release_manager.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 import os -import sys import re import subprocess +import sys +from urllib.parse import urlparse + import requests import simplejson -github_token = os.environ['GITHUB_TOKEN'] -auth = (github_token, 'x-oauth-basic') -repo = 'getredash/redash' +github_token = os.environ["GITHUB_TOKEN"] +auth = (github_token, "x-oauth-basic") +repo = "getredash/redash" + def _github_request(method, path, params=None, headers={}): - if not path.startswith('https://api.github.com'): + if urlparse(path).hostname != "api.github.com": url = "https://api.github.com/{}".format(path) else: url = path @@ -22,15 +25,18 @@ def _github_request(method, path, params=None, headers={}): response = requests.request(method, url, data=params, auth=auth) return response + def exception_from_error(message, response): - return Exception("({}) {}: {}".format(response.status_code, message, response.json().get('message', '?'))) + return Exception("({}) {}: {}".format(response.status_code, message, response.json().get("message", "?"))) + def rc_tag_name(version): return "v{}-rc".format(version) + def get_rc_release(version): tag = rc_tag_name(version) - response = _github_request('get', 'repos/{}/releases/tags/{}'.format(repo, tag)) + response = _github_request("get", "repos/{}/releases/tags/{}".format(repo, tag)) if response.status_code == 404: return None @@ -39,84 +45,101 @@ def get_rc_release(version): raise exception_from_error("Unknown error while looking RC release: ", response) + def create_release(version, commit_sha): tag = rc_tag_name(version) params = { - 'tag_name': tag, - 'name': "{} - RC".format(version), - 'target_commitish': commit_sha, - 'prerelease': True + "tag_name": tag, + "name": "{} - RC".format(version), + "target_commitish": commit_sha, + "prerelease": True, } - response = _github_request('post', 'repos/{}/releases'.format(repo), params) + response = _github_request("post", "repos/{}/releases".format(repo), params) if response.status_code != 201: raise exception_from_error("Failed creating new release", response) return response.json() + def upload_asset(release, filepath): - upload_url = release['upload_url'].replace('{?name,label}', '') - filename = filepath.split('/')[-1] + upload_url = release["upload_url"].replace("{?name,label}", "") + filename = filepath.split("/")[-1] with open(filepath) as file_content: - headers = {'Content-Type': 'application/gzip'} - response = requests.post(upload_url, file_content, params={'name': filename}, headers=headers, auth=auth, verify=False) + headers = {"Content-Type": "application/gzip"} + response = requests.post( + upload_url, file_content, params={"name": filename}, headers=headers, auth=auth, verify=False + ) if response.status_code != 201: # not 200/201/... - raise exception_from_error('Failed uploading asset', response) + raise exception_from_error("Failed uploading asset", response) return response + def remove_previous_builds(release): - for asset in release['assets']: - response = _github_request('delete', asset['url']) + for asset in release["assets"]: + response = _github_request("delete", asset["url"]) if response.status_code != 204: raise exception_from_error("Failed deleting asset", response) + def get_changelog(commit_sha): - latest_release = _github_request('get', 'repos/{}/releases/latest'.format(repo)) + latest_release = _github_request("get", "repos/{}/releases/latest".format(repo)) if latest_release.status_code != 200: - raise exception_from_error('Failed getting latest release', latest_release) + raise exception_from_error("Failed getting latest release", latest_release) latest_release = latest_release.json() - previous_sha = latest_release['target_commitish'] - - args = ['git', '--no-pager', 'log', '--merges', '--grep', 'Merge pull request', '--pretty=format:"%h|%s|%b|%p"', '{}...{}'.format(previous_sha, commit_sha)] + previous_sha = latest_release["target_commitish"] + + args = [ + "git", + "--no-pager", + "log", + "--merges", + "--grep", + "Merge pull request", + '--pretty=format:"%h|%s|%b|%p"', + "{}...{}".format(previous_sha, commit_sha), + ] log = subprocess.check_output(args) - changes = ["Changes since {}:".format(latest_release['name'])] + changes = ["Changes since {}:".format(latest_release["name"])] - for line in log.split('\n'): + for line in log.split("\n"): try: - sha, subject, body, parents = line[1:-1].split('|') + sha, subject, body, parents = line[1:-1].split("|") except ValueError: continue try: - pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0] + pull_request = re.match(r"Merge pull request #(\d+)", subject).groups()[0] pull_request = " #{}".format(pull_request) - except Exception as ex: + except Exception: pull_request = "" - author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1] + author = subprocess.check_output(["git", "log", "-1", '--pretty=format:"%an"', parents.split(" ")[-1]])[1:-1] changes.append("{}{}: {} ({})".format(sha, pull_request, body.strip(), author)) return "\n".join(changes) + def update_release_commit_sha(release, commit_sha): params = { - 'target_commitish': commit_sha, + "target_commitish": commit_sha, } - response = _github_request('patch', 'repos/{}/releases/{}'.format(repo, release['id']), params) + response = _github_request("patch", "repos/{}/releases/{}".format(repo, release["id"]), params) if response.status_code != 200: raise exception_from_error("Failed updating commit sha for existing release", response) return response.json() + def update_release(version, build_filepath, commit_sha): try: release = get_rc_release(version) @@ -125,21 +148,22 @@ def update_release(version, build_filepath, commit_sha): else: release = create_release(version, commit_sha) - print("Using release id: {}".format(release['id'])) + print("Using release id: {}".format(release["id"])) remove_previous_builds(release) response = upload_asset(release, build_filepath) changelog = get_changelog(commit_sha) - response = _github_request('patch', release['url'], {'body': changelog}) + response = _github_request("patch", release["url"], {"body": changelog}) if response.status_code != 200: raise exception_from_error("Failed updating release description", response) except Exception as ex: print(ex) -if __name__ == '__main__': + +if __name__ == "__main__": commit_sha = sys.argv[1] version = sys.argv[2] filepath = sys.argv[3] diff --git a/bin/requirements_wsl.txt b/bin/requirements_wsl.txt deleted file mode 100644 index 31beff4a9..000000000 --- a/bin/requirements_wsl.txt +++ /dev/null @@ -1,82 +0,0 @@ -# this file is used to vs-code to provide intellisense for the python code snippets -Flask==1.1.1 -Jinja2==2.11.3 -itsdangerous==1.1.0 -click==6.7 -MarkupSafe==1.1.1 -# Flask>=2.0.0 -# Jinja2>=3.0.0 -# itsdangerous>=2.0.0 -# click>=7.1.2 -# MarkupSafe>=2.0.0 -pyOpenSSL==19.0.0 -httplib2==0.19.0 -wtforms==2.2.1 -Flask-RESTful==0.3.7 -Flask-Login==0.4.1 -Flask-OAuthLib==0.9.5 -Flask-SQLAlchemy==2.4.1 -Flask-Migrate==2.5.2 -flask-mail==0.9.1 -flask-talisman==0.7.0 -Flask-Limiter==0.9.3 -Flask-WTF==0.14.3 -passlib==1.7.1 -aniso8601==8.0.0 -blinker==1.4 -# psycopg2==2.8.3 -python-dateutil==2.8.0 -pytz>=2019.3 -PyYAML==5.4 -Cerberus==1.3.2 -redis==3.3.11 -requests==2.31.0 -SQLAlchemy==1.3.10 -# We can't upgrade SQLAlchemy-Searchable version as newer versions require PostgreSQL > 9.6, but we target older versions at the moment. -SQLAlchemy-Searchable==0.10.6 -# We need to pin the version of pyparsing, as newer versions break SQLAlchemy-Searchable-10.0.6 (newer versions no longer depend on it) -pyparsing==2.4.2 -SQLAlchemy-Utils==0.34.2 -sqlparse==0.4.4 -statsd==3.3.0 -gunicorn==20.0.4 -rq==1.1.0 -rq-scheduler==0.10.0 -jsonschema==3.1.1 -RestrictedPython==5.0 -pysaml2==6.5.0 -pycrypto==2.6.1 -funcy==1.13 -sentry-sdk>=0.14.3,<0.15.0 -semver==2.8.1 -xlsxwriter==1.2.2 -# pystache==0.5.4 -parsedatetime==2.4 -PyJWT==1.7.1 -cryptography==3.3.2 -simplejson==3.16.0 -ua-parser==0.8.0 -user-agents==2.0 -maxminddb-geolite2==2018.703 -pypd==1.1.0 -disposable-email-domains>=0.0.52 -gevent==1.4.0 -sshtunnel==0.1.5 -supervisor==4.1.0 -supervisor_checks==0.8.1 -werkzeug==0.16.1 -urllib3 -# Install the dependencies of the bin/bundle-extensions script here. -# It has its own requirements file to simplify the frontend client build process --r requirements_bundles.txt -# Uncomment the requirement for ldap3 if using ldap. -# It is not included by default because of the GPL license conflict. -# ldap3==2.2.4 -greenlet==0.4.16 -mailchimp-marketing==3.0.36 -inflection==0.5.1 -mock==4.0.3 -lzstring==1.0.4 -pydash==5.0.1 -python-dotenv>=0.21.0 -regex==2023.8.8 \ No newline at end of file diff --git a/bin/restart_cypress.sh b/bin/restart_cypress.sh new file mode 100644 index 000000000..6b0f6e6f2 --- /dev/null +++ b/bin/restart_cypress.sh @@ -0,0 +1,16 @@ +# check if we are in client folder, use dirname +if [ "$(basename $(pwd))" != "client" ]; then + cd client +fi +if [ ! -d "node_modules" ]; then + npm install +fi + +npm run cypress build +npm run cypress start # also seeds the database + +if [ "$1" = "run" ]; then + npm run cypress run +else # if [ "$1" = "open" ]; then + npm run cypress open +fi diff --git a/bin/restart_plywood.sh b/bin/restart_plywood.sh index 94238539f..8ac421be6 100644 --- a/bin/restart_plywood.sh +++ b/bin/restart_plywood.sh @@ -1,2 +1,13 @@ -cd client && npm run build:plywood && npm run build:plywood-server && cd .. -docker-compose stop plywood && docker-compose rm -f plywood && docker-compose up -d plywood && docker-compose logs -f plywood +# this script restarts plywood with latest changes +# in order this script to work you need to use docker dev files +# runs it on local on debug mode +cd client +npm run build:plywood +npm run build:plywood-server +cd .. +docker-compose stop plywood +docker-compose rm -f plywood +docker-compose up -d plywood -d +docker-compose stop plywood +cd plywood +npm run dev:debug diff --git a/bin/restart_server.sh b/bin/restart_server.sh index 8c31a1c73..2a474e1de 100644 --- a/bin/restart_server.sh +++ b/bin/restart_server.sh @@ -6,27 +6,17 @@ else fi if [ "$state" = "yes" ] || [ "$state" = "first" ]; then - echo "Stopping and removing the server container" docker stop datareporter-server-1 docker rm datareporter-server-1 docker rmi datareporter-server - # export skip_dev_deps="do not update dev dependencies" - # export skip_ds_deps="do not update ds dependencies" - # export skip_frontend_build="do not build front-end" - docker-compose up -d - docker-compose stop server && docker-compose run --rm --service-ports server debug && docker-compose start server - else - echo "Restarting server without rebuilding the image" docker stop datareporter-server-1 docker rm datareporter-server-1 - # docker rmi datareporter-server - export skip_dev_deps="do not update dev dependencies" - export skip_ds_deps="do not update ds dependencies" export skip_frontend_build="do not build front-end" - docker-compose up -d - docker-compose stop server && docker-compose run --rm --service-ports server debug && docker-compose start server +fi -fi \ No newline at end of file +docker compose -f compose.dev.yml up -d +docker compose -f compose.dev.yml run --rm server create_db +docker compose -f compose.dev.yml stop server && docker compose -f compose.dev.yml run --rm --service-ports server debug && docker compose -f compose.dev.yml start server diff --git a/bin/restart_tests.sh b/bin/restart_tests.sh new file mode 100644 index 000000000..b0243a795 --- /dev/null +++ b/bin/restart_tests.sh @@ -0,0 +1,4 @@ +docker compose build +docker compose up -d +docker compose run --rm postgres psql -h postgres -U postgres -c "create database tests" # make sure tests database is created: +docker compose run --rm server tests \ No newline at end of file diff --git a/bin/restart_worker_server.sh b/bin/restart_worker_server.sh new file mode 100644 index 000000000..887591fb6 --- /dev/null +++ b/bin/restart_worker_server.sh @@ -0,0 +1,23 @@ +#!/bin/bash +if [ "$1" ]; then + state="$1" +else + state="no" +fi + +if [ "$state" = "yes" ] || [ "$state" = "first" ]; then + echo "Stopping and removing the worker server container" + docker stop datareporter-worker-server-1 + docker rm datareporter-worker-server-1 + docker rmi datareporter-worker-server +else + echo "Restarting worker server without rebuilding the image" + docker stop datareporter-worker-server-1 + docker rm datareporter-worker-server-1 + export skip_dev_deps="do not update dev dependencies" + export skip_ds_deps="do not update ds dependencies" + export skip_frontend_build="do not build front-end" +fi + +docker compose -f compose.dev.yml up -d +docker compose -f compose.dev.yml stop worker-server && docker compose -f compose.dev.yml run --rm --use-aliases -p 5001:5000 -p 5679:5678 worker-server dev_worker_server && docker compose -f compose.dev.yml start worker-server \ No newline at end of file diff --git a/bin/test_turnilo.sh b/bin/test_turnilo.sh new file mode 100644 index 000000000..90bfdb031 --- /dev/null +++ b/bin/test_turnilo.sh @@ -0,0 +1 @@ +cd client/app/components/TurniloComponent && npm run test:client \ No newline at end of file diff --git a/bin/upgrade b/bin/upgrade deleted file mode 100755 index 376866f1e..000000000 --- a/bin/upgrade +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 -import urllib -import argparse -import os -import subprocess -import sys -from collections import namedtuple -from fnmatch import fnmatch - -import requests - -try: - import semver -except ImportError: - print("Missing required library: semver.") - exit(1) - -REDASH_HOME = os.environ.get('REDASH_HOME', '/opt/redash') -CURRENT_VERSION_PATH = '{}/current'.format(REDASH_HOME) - - -def run(cmd, cwd=None): - if not cwd: - cwd = REDASH_HOME - - return subprocess.check_output(cmd, cwd=cwd, shell=True, stderr=subprocess.STDOUT) - - -def confirm(question): - reply = str(input(question + ' (y/n): ')).lower().strip() - - if reply[0] == 'y': - return True - if reply[0] == 'n': - return False - else: - return confirm("Please use 'y' or 'n'") - - -def version_path(version_name): - return "{}/{}".format(REDASH_HOME, version_name) - -END_CODE = '\033[0m' - - -def colored_string(text, color): - if sys.stdout.isatty(): - return "{}{}{}".format(color, text, END_CODE) - else: - return text - - -def h1(text): - print(colored_string(text, '\033[4m\033[1m')) - - -def green(text): - print(colored_string(text, '\033[92m')) - - -def red(text): - print(colored_string(text, '\033[91m')) - - -class Release(namedtuple('Release', ('version', 'download_url', 'filename', 'description'))): - def v1_or_newer(self): - return semver.compare(self.version, '1.0.0-alpha') >= 0 - - def is_newer(self, version): - return semver.compare(self.version, version) > 0 - - @property - def version_name(self): - return self.filename.replace('.tar.gz', '') - - -def get_latest_release_from_ci(): - response = requests.get('https://circleci.com/api/v1.1/project/github/getredash/redash/latest/artifacts?branch=master') - - if response.status_code != 200: - exit("Failed getting releases (status code: %s)." % response.status_code) - - tarball_asset = filter(lambda asset: asset['url'].endswith('.tar.gz'), response.json())[0] - filename = urllib.unquote(tarball_asset['pretty_path'].split('/')[-1]) - version = filename.replace('redash.', '').replace('.tar.gz', '') - - release = Release(version, tarball_asset['url'], filename, '') - - return release - - -def get_release(channel): - if channel == 'ci': - return get_latest_release_from_ci() - - response = requests.get('https://version.redash.io/api/releases?channel={}'.format(channel)) - release = response.json()[0] - - filename = release['download_url'].split('/')[-1] - release = Release(release['version'], release['download_url'], filename, release['description']) - - return release - - -def link_to_current(version_name): - green("Linking to current version...") - run('ln -nfs {} {}'.format(version_path(version_name), CURRENT_VERSION_PATH)) - - -def restart_services(): - # We're doing this instead of simple 'supervisorctl restart all' because - # otherwise it won't notice that /opt/redash/current pointing at a different - # directory. - green("Restarting...") - try: - run('sudo /etc/init.d/redash_supervisord restart') - except subprocess.CalledProcessError as e: - run('sudo service supervisor restart') - - -def update_requirements(version_name): - green("Installing new Python packages (if needed)...") - new_requirements_file = '{}/requirements.txt'.format(version_path(version_name)) - - install_requirements = False - - try: - run('diff {}/requirements.txt {}'.format(CURRENT_VERSION_PATH, new_requirements_file)) != 0 - except subprocess.CalledProcessError as e: - if e.returncode != 0: - install_requirements = True - - if install_requirements: - run('sudo pip install -r {}'.format(new_requirements_file)) - - -def apply_migrations(release): - green("Running migrations (if needed)...") - if not release.v1_or_newer(): - return apply_migrations_pre_v1(release.version_name) - - run("sudo -u redash bin/run ./manage.py db upgrade", cwd=version_path(release.version_name)) - - -def find_migrations(version_name): - current_migrations = set([f for f in os.listdir("{}/migrations".format(CURRENT_VERSION_PATH)) if fnmatch(f, '*_*.py')]) - new_migrations = sorted([f for f in os.listdir("{}/migrations".format(version_path(version_name))) if fnmatch(f, '*_*.py')]) - - return [m for m in new_migrations if m not in current_migrations] - - -def apply_migrations_pre_v1(version_name): - new_migrations = find_migrations(version_name) - - if new_migrations: - green("New migrations to run: ") - print(', '.join(new_migrations)) - else: - print("No new migrations in this version.") - - if new_migrations and confirm("Apply new migrations? (make sure you have backup)"): - for migration in new_migrations: - print("Applying {}...".format(migration)) - run("sudo sudo -u redash PYTHONPATH=. bin/run python migrations/{}".format(migration), cwd=version_path(version_name)) - - -def download_and_unpack(release): - directory_name = release.version_name - - green("Downloading release tarball...") - run('sudo wget --header="Accept: application/octet-stream" -O {} {}'.format(release.filename, release.download_url)) - green("Unpacking to: {}...".format(directory_name)) - run('sudo mkdir -p {}'.format(directory_name)) - run('sudo tar -C {} -xvf {}'.format(directory_name, release.filename)) - - green("Changing ownership to redash...") - run('sudo chown redash {}'.format(directory_name)) - - green("Linking .env file...") - run('sudo ln -nfs {}/.env {}/.env'.format(REDASH_HOME, version_path(directory_name))) - - -def current_version(): - real_current_path = os.path.realpath(CURRENT_VERSION_PATH).replace('.b', '+b') - return real_current_path.replace(REDASH_HOME + '/', '').replace('redash.', '') - - -def verify_minimum_version(): - green("Current version: " + current_version()) - if semver.compare(current_version(), '0.12.0') < 0: - red("You need to have Redash v0.12.0 or newer to upgrade to post v1.0.0 releases.") - green("To upgrade to v0.12.0, run the upgrade script set to the legacy channel (--channel legacy).") - exit(1) - - -def show_description_and_confirm(description): - if description: - print(description) - - if not confirm("Continue with upgrade?"): - red("Cancelling upgrade.") - exit(1) - - -def verify_newer_version(release): - if not release.is_newer(current_version()): - red("The found release is not newer than your current deployed release ({}).".format(current_version())) - if not confirm("Continue with upgrade?"): - red("Cancelling upgrade.") - exit(1) - - -def deploy_release(channel): - h1("Starting Redash upgrade:") - - release = get_release(channel) - green("Found version: {}".format(release.version)) - - if release.v1_or_newer(): - verify_minimum_version() - - verify_newer_version(release) - show_description_and_confirm(release.description) - - try: - download_and_unpack(release) - update_requirements(release.version_name) - apply_migrations(release) - link_to_current(release.version_name) - restart_services() - green("Done! Enjoy.") - except subprocess.CalledProcessError as e: - red("Failed running: {}".format(e.cmd)) - red("Exit status: {}\nOutput:\n{}".format(e.returncode, e.output)) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("--channel", help="The channel to get release from (default: stable).", default='stable') - args = parser.parse_args() - - deploy_release(args.channel) diff --git a/client/.babelrc b/client/.babelrc index 1773f9500..af5a043b4 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1,23 +1,24 @@ { "presets": [ - ["@babel/preset-env", { - "exclude": [ - "@babel/plugin-transform-async-to-generator", - "@babel/plugin-transform-arrow-functions" - ], - "useBuiltIns": "usage" - }], + [ + "@babel/preset-env", + { + "exclude": ["@babel/plugin-transform-async-to-generator", "@babel/plugin-transform-arrow-functions"], + "corejs": "2", + "useBuiltIns": "usage" + } + ], "@babel/preset-react", - "@babel/preset-typescript", - "@babel/preset-flow" + "@babel/preset-typescript" ], "plugins": [ - "babel-plugin-flow-to-typescript", "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-object-assign", - ["babel-plugin-transform-builtin-extend", { - "globals": ["Error"] - }], - "@babel/plugin-transform-runtime" + [ + "babel-plugin-transform-builtin-extend", + { + "globals": ["Error"] + } + ] ] } diff --git a/client/.eslintignore b/client/.eslintignore deleted file mode 100644 index 013f6ab6e..000000000 --- a/client/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -build/*.js -dist -config/*.js -client/dist diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 8bc0055d0..1bcb72d19 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,40 +1,169 @@ module.exports = { root: true, parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, extends: [ "react-app", - "plugin:compat/recommended", "prettier", - // Remove any typescript-eslint rules that would conflict with prettier - "prettier/@typescript-eslint", + "plugin:compat/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:jsx-a11y/recommended", + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", // This tells ESLint about the new JSX transform ], - plugins: ["jest", "compat", "no-only-tests", "@typescript-eslint"], + plugins: ["jest", "prettier", "compat", "no-only-tests", "@typescript-eslint", "jsx-a11y", "cypress", "react"], settings: { "import/resolver": "webpack", + react: { + version: "detect", + }, }, env: { browser: true, node: true, }, rules: { + "no-empty": ["warn", { allowEmptyCatch: true }], // allow debugger during development "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, - "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/anchor-is-valid": [ + // TMP + "off", + { + components: ["Link"], + aspects: ["noHref", "invalidHref", "preferButton"], + }, + ], + "jsx-a11y/no-redundant-roles": "error", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/click-events-have-key-events": "off", // TMP + "jsx-a11y/no-static-element-interactions": "off", // TMP + "jsx-a11y/no-noninteractive-element-interactions": "off", // TMP + "jsx-a11y/label-has-associated-control": "off", + "no-console": ["warn", { allow: ["warn", "error"] }], + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "antd", + message: "Please use 'import XXX from antd/lib/XXX' import instead.", + }, + { + name: "antd/lib", + message: "Please use 'import XXX from antd/lib/XXX' import instead.", + }, + ], + }, + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-use-before-define": "warn", + "@typescript-eslint/ban-types": "warn", + "@typescript-eslint/explicit-module-boundary-types": "warn", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "warn", + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off", + "react/jsx-uses-vars": "warn", + "react/jsx-no-target-blank": "warn", + "react/no-string-refs": "warn", + "react/no-children-prop": "warn", + "react/no-direct-mutation-state": "warn", + "react/no-unknown-property": "warn", + "react/no-deprecated": "warn", + "react/no-unescaped-entities": "off", + "react/jsx-key": "warn", + "react/no-find-dom-node": "off", + "react/display-name": "off", + "react/jsx-no-comment-textnodes": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn", { varsIgnorePattern: "^React$" }], + "no-case-declarations": "off", + "react/prop-types": "off", + "compat/compat": "warn", + "@typescript-eslint/ban-ts-comment": [ + "warn", + { + "ts-ignore": "allow-with-description", + minimumDescriptionLength: 3, + }, + ], + "no-useless-escape": "warn", + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": "warn", }, overrides: [ { - // Only run typescript-eslint on TS files - files: ["*.ts", "*.tsx", ".*.ts", ".*.tsx"], - extends: ["plugin:@typescript-eslint/recommended"], + files: ["**/*.js", "**/*.jsx"], rules: { - // Do not require functions (especially react components) to have explicit returns - "@typescript-eslint/explicit-function-return-type": "off", - // Do not require to type every import from a JS file to speed up development + "react/react-in-jsx-scope": "off", + "react/display-name": "off", + "react/forbid-prop-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", - // Do not complain about useless contructors in declaration files - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-useless-constructor": "off", + "@typescript-eslint/no-redeclare": "off", + "no-redeclare": "warn", // Re-enable base rule for JS files + "no-unused-vars": ["warn", { varsIgnorePattern: "^React$" }], // Re-enable base rule for JS files + "react/prop-types": "warn", + }, + }, + { + files: ["**/*.mocha.ts", "**/*.mocha.tsx", "**/*.test.ts", "**/*.test.tsx"], + parser: null, + env: { + mocha: true, + jest: false, + }, + plugins: [], + rules: { + "@typescript-eslint/no-empty-function": "off", + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "off", + "jest/no-disabled-tests": "off", + "jest/valid-expect": "off", + "no-var": "warn", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + }, + }, + { + files: ["**/TurniloComponent/**/*.{js,jsx,ts,tsx}"], + rules: { + "@typescript-eslint/no-empty-interface": "off", + "no-var": "warn", + "prefer-const": "warn", + "@typescript-eslint/no-namespace": ["warn", { allowDeclarations: true }], + "getter-return": "off", + "@typescript-eslint/no-empty-function": "off", + "jest/no-done-callback": "off", + "no-undef": "error", + "@typescript-eslint/no-unused-expressions": "off", + "no-useless-catch": "off", + }, + }, + { + files: ["**/__tests__/**/*.{js,jsx,ts,tsx}"], + rules: { + "no-console": "off", }, }, ], + ignorePatterns: ["**/*.min.js", "build/*.js", "dist", "config/*.js", "client/dist", "node_modules"], }; diff --git a/client/.percy.yml b/client/.percy.yml new file mode 100644 index 000000000..b1a1baa67 --- /dev/null +++ b/client/.percy.yml @@ -0,0 +1,7 @@ +version: 2 +snapshot: + widths: [768, 1280] + minHeight: 1024 + percyCSS: | + .loading-spinner { display: none !important; } + .timestamp { display: none !important; } diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 000000000..db4c6d9b6 --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/client/Makefile b/client/Makefile new file mode 100644 index 000000000..f09ed5ec7 --- /dev/null +++ b/client/Makefile @@ -0,0 +1,4 @@ +.PHONY: fmt + +fmt: + $(MAKE) -C .. fmt \ No newline at end of file diff --git a/client/app/.eslintrc.js b/client/app/.eslintrc.js index 85a37b82a..9a687d3de 100644 --- a/client/app/.eslintrc.js +++ b/client/app/.eslintrc.js @@ -1,9 +1,13 @@ module.exports = { - extends: ["plugin:jest/recommended"], - plugins: ["jest"], + extends: ["../.eslintrc.js"], env: { "jest/globals": true, }, + globals: { + int: "readonly", + ClientRect: "readonly", + JSX: "readonly", + }, rules: { "jest/no-focused-tests": "off", }, diff --git a/client/app/__tests__/jest-setup.test.js b/client/app/__tests__/jest-setup.test.js new file mode 100644 index 000000000..c408b77fb --- /dev/null +++ b/client/app/__tests__/jest-setup.test.js @@ -0,0 +1,39 @@ +/** + * Test to verify Jest and testing library setup + */ + +describe("Jest Setup", () => { + test("Jest is working correctly", () => { + expect(1 + 1).toBe(2); + }); + + test("can import React Testing Library", () => { + const { render } = require("@testing-library/react"); + expect(render).toBeDefined(); + }); + + test("jest-dom matchers are available", () => { + // Test if jest-dom matchers are loaded + const div = document.createElement("div"); + div.textContent = "Hello"; + + // Use basic Jest matchers first + expect(div.textContent).toBe("Hello"); + + // Try jest-dom matcher if available + try { + expect(div).toBeInTheDocument(); + console.log("✅ jest-dom matchers are available"); + } catch (e) { + console.log( + "ℹ️ jest-dom matchers not available, using basic Jest matchers", + ); + } + }); + + test("DOM environment is set up", () => { + expect(document).toBeDefined(); + expect(window).toBeDefined(); + expect(global.window).toBeDefined(); + }); +}); diff --git a/client/app/__tests__/percy-module.test.js b/client/app/__tests__/percy-module.test.js new file mode 100644 index 000000000..12d74cdce --- /dev/null +++ b/client/app/__tests__/percy-module.test.js @@ -0,0 +1,102 @@ +const percySnapshot = require("@percy/puppeteer"); + +/** + * Test for Percy module availability and configuration + * This addresses the "Cannot find module '/usr/src/app/client/percy'" error + */ + +describe("Percy Module Configuration", () => { + beforeEach(() => { + // Clear any cached modules + jest.resetModules(); + }); + + test("percy module should be available or gracefully handled", () => { + // Test if percy module exists in expected location + const percyPath = "/usr/src/app/client/percy"; + + try { + // Try to require the percy module + require(percyPath); + console.log("✅ Percy module found at expected location"); + } catch (error) { + // Expected in local development - percy is typically only available in Docker + expect(error.code).toBe("MODULE_NOT_FOUND"); + console.log( + "ℹ️ Percy module not found locally (expected in development)", + ); + + // Verify we can mock percy for testing + jest.doMock(percyPath, () => ({ + exec: jest.fn(), + snapshot: jest.fn(), + isRunning: jest.fn(() => false), + })); + + const mockedPercy = require(percyPath); + expect(mockedPercy.exec).toBeDefined(); + expect(mockedPercy.snapshot).toBeDefined(); + expect(mockedPercy.isRunning).toBeDefined(); + + console.log("✅ Percy module successfully mocked for testing"); + } + }); + + test("percy environment variables should be properly configured", () => { + const requiredPercyVars = [ + "PERCY_TOKEN", + "PERCY_PROJECT", + "PERCY_PARALLEL_TOTAL", + "PERCY_PARALLEL_NONCE", + "PERCY_PARALLEL", + "PERCY_BRANCH", + "PERCY_COMMIT", + "PERCY_PULL_REQUEST", + ]; + + // In CI environment, these should be set + if (process.env.CI) { + requiredPercyVars.forEach(varName => { + expect(process.env[varName]).toBeDefined(); + }); + } else { + // In local development, log which vars are missing + const missingVars = requiredPercyVars.filter( + varName => !process.env[varName], + ); + if (missingVars.length > 0) { + console.log( + `ℹ️ Missing Percy environment variables (expected locally): ${missingVars.join(", ")}`, + ); + } + } + }); + + test("cypress should handle percy module gracefully", () => { + // Mock scenario where percy module is not available + const mockCypressTask = { + percySnapshot: (name, options = {}) => { + try { + // Simulate percy module loading + const percy = require("/usr/src/app/client/percy"); + return percy.snapshot(name, options); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + console.log(`⚠️ Percy not available, skipping snapshot: ${name}`); + return Promise.resolve(); + } + throw error; + } + }, + }; + + // Test that the task handles missing percy gracefully + expect(() => { + mockCypressTask.percySnapshot("test-snapshot"); + }).not.toThrow(); + }); +}); + +test("Ensure Percy module is imported correctly", () => { + expect(percySnapshot).toBeDefined(); +}); diff --git a/client/app/__tests__/setupTests.js b/client/app/__tests__/setupTests.js new file mode 100644 index 000000000..ab0b82815 --- /dev/null +++ b/client/app/__tests__/setupTests.js @@ -0,0 +1,141 @@ +// Import jest-dom matchers +import "@testing-library/jest-dom/extend-expect"; + +// Suppress console warnings and logs for cleaner test output +const originalWarn = console.warn; +const originalError = console.error; +const originalLog = console.log; + +beforeAll(() => { + // Suppress Moment.js warnings + console.warn = (...args) => { + if ( + typeof args[0] === "string" && + (args[0].includes("Warning: ReactDOM.render is deprecated") || + args[0].includes("Warning: componentWillMount has been renamed") || + args[0].includes( + "Deprecation warning: value provided is not in a recognized RFC2822 or ISO format", + )) + ) { + return; + } + originalWarn.call(console, ...args); + }; + + // Suppress React error boundary logs in tests (they're expected) + console.error = (...args) => { + if ( + typeof args[0] === "string" && + (args[0].includes( + "The above error occurred in the component", + ) || + args[0].includes("React will try to recreate this component tree")) + ) { + return; + } + originalError.call(console, ...args); + }; + + // Suppress error boundary console.log messages in tests + console.log = (...args) => { + if ( + typeof args[0] === "string" && + args[0].includes("Error caught by boundary:") + ) { + return; + } + originalLog.call(console, ...args); + }; +}); + +afterAll(() => { + console.warn = originalWarn; + console.error = originalError; + console.log = originalLog; +}); + +// Configure Enzyme for React 16 (if available) +try { + const { configure } = require("enzyme"); + const Adapter = require("enzyme-adapter-react-16"); + configure({ adapter: new Adapter() }); +} catch (e) { + // Enzyme not available, using React Testing Library only +} + +// Mock window.matchMedia +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +// Mock IntersectionObserver +global.IntersectionObserver = class IntersectionObserver { + constructor() {} + observe() { + return null; + } + disconnect() { + return null; + } + unobserve() { + return null; + } +}; + +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + constructor() {} + observe() { + return null; + } + disconnect() { + return null; + } + unobserve() { + return null; + } +}; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; +global.localStorage = localStorageMock; + +// Mock sessionStorage +const sessionStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; +global.sessionStorage = sessionStorageMock; + +// Mock URL.createObjectURL +global.URL.createObjectURL = jest.fn(); + +// Mock moment for better date handling in tests +jest.mock("moment", () => { + const actualMoment = jest.requireActual("moment"); + + return date => { + // Handle invalid dates gracefully in tests + if (date === "value" || date === undefined || date === null) { + return actualMoment("2023-01-01"); // Return a valid default date + } + return actualMoment(date); + }; +}); diff --git a/client/app/assets/css/login.css b/client/app/assets/css/login.css index cf46eefb0..ed84c4b6c 100644 --- a/client/app/assets/css/login.css +++ b/client/app/assets/css/login.css @@ -1,6 +1,6 @@ body { padding-top: 0px !important; - background-color: #FFFFFF; + background-color: #ffffff; } .logo-container { @@ -66,4 +66,3 @@ img.login-button { margin-left: auto; margin-right: auto; } - diff --git a/client/app/assets/fonts/uifont/uifont-line-demo.html b/client/app/assets/fonts/uifont/uifont-line-demo.html index 3c062584c..7e0e3221c 100644 --- a/client/app/assets/fonts/uifont/uifont-line-demo.html +++ b/client/app/assets/fonts/uifont/uifont-line-demo.html @@ -1,477 +1,981 @@ - + - - - - - - - - - - UI Font Stroked Regular Specimen - - - - - - -
- - - -
- - -
- -
-
-
AaBb
-
-
- -
-
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
-
-
-
- - - - - - - - - - - - - - - - -
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
- -
- -
- - - -
- - -
-
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
-
- bodyUI Font Stroked Regular -
-
- bodyArial -
-
- bodyVerdana -
-
- bodyGeorgia -
- - - -
- - -
- -
-

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
-
-
-

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
- -
- -
- -
-
-

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
-

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
- -
- -
- -
-
-

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
- -
- - - -
-
-

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
- -
-
-

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
- -
-
-

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
-

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
- -
- -
- -
-
-

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
- -
- - - - -
- -
- -
- -
-

Lorem Ipsum Dolor

-

Etiam porta sem malesuada magna mollis euismod

- - -
-
-
-
-

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

- - -

Pellentesque ornare sem

- -

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

- -

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

- -

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

- -

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

- -

Cras mattis consectetur

- -

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

- -

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

-
- - -
- -
- - - - - - -
-
-
- -

Language Support

-

The subset of UI Font Stroked Regular in this kit supports the following languages:
- - English, Arrernte, Bislama, Cebuano, Fijian, Gilbertese, Hmong, Ibanag, Iloko_ilokano, Interglossa_glosa, Interlingua, Lojban, Norfolk_pitcairnese, Oromo, Rotokas, Seychelles_creole, Shona, Somali, Southern_ndebele, Swahili, Swati_swazi, Tok_pisin, Warlpiri, Xhosa, Zulu, Latinbasic, Demo

-

Glyph Chart

-

The subset of UI Font Stroked Regular in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.

-
- -

&#32;

-

&#45;

-
-

&#46;

.
-

&#48;

0
-

&#49;

1
-

&#50;

2
-

&#51;

3
-

&#52;

4
-

&#53;

5
-

&#54;

6
-

&#55;

7
-

&#56;

8
-

&#57;

9
-

&#65;

A
-

&#66;

B
-

&#67;

C
-

&#68;

D
-

&#69;

E
-

&#70;

F
-

&#71;

G
-

&#72;

H
-

&#73;

I
-

&#74;

J
-

&#75;

K
-

&#76;

L
-

&#77;

M
-

&#78;

N
-

&#79;

O
-

&#80;

P
-

&#81;

Q
-

&#82;

R
-

&#83;

S
-

&#84;

T
-

&#85;

U
-

&#86;

V
-

&#87;

W
-

&#88;

X
-

&#89;

Y
-

&#90;

Z
-

&#95;

_
-

&#97;

a
-

&#98;

b
-

&#99;

c
-

&#100;

d
-

&#101;

e
-

&#102;

f
-

&#103;

g
-

&#104;

h
-

&#105;

i
-

&#106;

j
-

&#107;

k
-

&#108;

l
-

&#109;

m
-

&#110;

n
-

&#111;

o
-

&#112;

p
-

&#113;

q
-

&#114;

r
-

&#115;

s
-

&#116;

t
-

&#117;

u
-

&#118;

v
-

&#119;

w
-

&#120;

x
-

&#121;

y
-

&#122;

z
-

&#160;

 
-

&#173;

­
-

&#8192;

 
-

&#8193;

-

&#8194;

-

&#8195;

-

&#8196;

-

&#8197;

-

&#8198;

-

&#8199;

-

&#8200;

-

&#8201;

-

&#8202;

-

&#8208;

-

&#8209;

-

&#8210;

-

&#8211;

-

&#8212;

-

&#8239;

-

&#8287;

-

&#9724;

-
-
- - -
-
- - -
- -
- -
-
-
-

Installing Webfonts

- -

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

- -

1. Upload your webfonts

-

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

- -

2. Include the webfont stylesheet

-

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

- - - -@font-face{ - font-family: 'MyWebFont'; - src: url('WebFont.eot'); - src: url('WebFont.eot?#iefix') format('embedded-opentype'), - url('WebFont.woff') format('woff'), - url('WebFont.ttf') format('truetype'), - url('WebFont.svg#webfont') format('svg'); -} - - -

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

- <link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" /> - -

3. Modify your own stylesheet

-

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

-p { font-family: 'WebFont', Arial, sans-serif; } - -

4. Test

-

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

-
- - -
- -
- -
- -
- - \ No newline at end of file + + + + + + + + + + UI Font Stroked Regular Specimen + + + + + +
+ + + +
+
+
+
+
AaBb
+
+
+ +
+
+ A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​; +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
10 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
11 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
12 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
13 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
14 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
16 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
18 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
20 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
24 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
30 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
36 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
48 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
60 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
72 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
90 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
+
+
+ +
+
+
+ ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body +
+
body
+
body
+
body
+
+
bodyUI Font Stroked Regular
+
bodyArial
+
bodyVerdana
+
bodyGeorgia
+
+ +
+
+

+ 10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+
+
+

+ 14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+
+ +
+
+
+

Lorem Ipsum Dolor

+

Etiam porta sem malesuada magna mollis euismod

+ + +
+
+
+
+

+ Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus + ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +

+ +

Pellentesque ornare sem

+ +

+ Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec + ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. + Nullam id dolor id nibh ultricies vehicula ut id elit. +

+ +

+ Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit + amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. +

+ +

+ Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Aenean lacinia bibendum nulla sed consectetur. +

+ +

+ Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu + leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus + auctor fringilla. +

+ +

Cras mattis consectetur

+ +

+ Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum + nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis + consectetur purus sit amet fermentum. +

+ +

+ Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu + leo. Cras mattis consectetur purus sit amet fermentum. +

+
+ + +
+
+ +
+
+
+

Language Support

+

+ The subset of UI Font Stroked Regular in this kit supports the following languages:
+ + English, Arrernte, Bislama, Cebuano, Fijian, Gilbertese, Hmong, Ibanag, Iloko_ilokano, + Interglossa_glosa, Interlingua, Lojban, Norfolk_pitcairnese, Oromo, Rotokas, Seychelles_creole, Shona, + Somali, Southern_ndebele, Swahili, Swati_swazi, Tok_pisin, Warlpiri, Xhosa, Zulu, Latinbasic, Demo +

+

Glyph Chart

+

+ The subset of UI Font Stroked Regular in this kit includes all the glyphs listed below. Unicode entities + are included above each glyph to help you insert individual characters into your layout. +

+
+
+

&#32;

+ +
+
+

&#45;

+ - +
+
+

&#46;

+ . +
+
+

&#48;

+ 0 +
+
+

&#49;

+ 1 +
+
+

&#50;

+ 2 +
+
+

&#51;

+ 3 +
+
+

&#52;

+ 4 +
+
+

&#53;

+ 5 +
+
+

&#54;

+ 6 +
+
+

&#55;

+ 7 +
+
+

&#56;

+ 8 +
+
+

&#57;

+ 9 +
+
+

&#65;

+ A +
+
+

&#66;

+ B +
+
+

&#67;

+ C +
+
+

&#68;

+ D +
+
+

&#69;

+ E +
+
+

&#70;

+ F +
+
+

&#71;

+ G +
+
+

&#72;

+ H +
+
+

&#73;

+ I +
+
+

&#74;

+ J +
+
+

&#75;

+ K +
+
+

&#76;

+ L +
+
+

&#77;

+ M +
+
+

&#78;

+ N +
+
+

&#79;

+ O +
+
+

&#80;

+ P +
+
+

&#81;

+ Q +
+
+

&#82;

+ R +
+
+

&#83;

+ S +
+
+

&#84;

+ T +
+
+

&#85;

+ U +
+
+

&#86;

+ V +
+
+

&#87;

+ W +
+
+

&#88;

+ X +
+
+

&#89;

+ Y +
+
+

&#90;

+ Z +
+
+

&#95;

+ _ +
+
+

&#97;

+ a +
+
+

&#98;

+ b +
+
+

&#99;

+ c +
+
+

&#100;

+ d +
+
+

&#101;

+ e +
+
+

&#102;

+ f +
+
+

&#103;

+ g +
+
+

&#104;

+ h +
+
+

&#105;

+ i +
+
+

&#106;

+ j +
+
+

&#107;

+ k +
+
+

&#108;

+ l +
+
+

&#109;

+ m +
+
+

&#110;

+ n +
+
+

&#111;

+ o +
+
+

&#112;

+ p +
+
+

&#113;

+ q +
+
+

&#114;

+ r +
+
+

&#115;

+ s +
+
+

&#116;

+ t +
+
+

&#117;

+ u +
+
+

&#118;

+ v +
+
+

&#119;

+ w +
+
+

&#120;

+ x +
+
+

&#121;

+ y +
+
+

&#122;

+ z +
+
+

&#160;

+   +
+
+

&#173;

+ ­ +
+
+

&#8192;

+   +
+
+

&#8193;

+   +
+
+

&#8194;

+   +
+
+

&#8195;

+   +
+
+

&#8196;

+   +
+
+

&#8197;

+   +
+
+

&#8198;

+   +
+
+

&#8199;

+   +
+
+

&#8200;

+   +
+
+

&#8201;

+   +
+
+

&#8202;

+   +
+
+

&#8208;

+ ‐ +
+
+

&#8209;

+ ‑ +
+
+

&#8210;

+ ‒ +
+
+

&#8211;

+ – +
+
+

&#8212;

+ — +
+
+

&#8239;

+   +
+
+

&#8287;

+   +
+
+

&#9724;

+ ◼ +
+
+
+
+
+ +
+ +
+
+
+

Installing Webfonts

+ +

+ Webfonts are supported by all major browser platforms but not all in the same way. There are currently + four different font formats that must be included in order to target all browsers. This includes TTF, + WOFF, EOT and SVG. +

+ +

1. Upload your webfonts

+

+ You must upload your webfont kit to your website. They should be in or near the same directory as your + CSS files. +

+ +

2. Include the webfont stylesheet

+

+ A special CSS @font-face declaration helps the various browsers select the appropriate font it needs + without causing you a bunch of headaches. Learn more about this syntax by reading the + Fontspring blog post + about it. The code for it is as follows: +

+ + + @font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?#iefix') + format('embedded-opentype'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), + url('WebFont.svg#webfont') format('svg'); } + + +

+ We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in + your HTML, like this: +

+ <link rel="stylesheet" href="stylesheet.css" type="text/css" + charset="utf-8" /> + +

3. Modify your own stylesheet

+

+ To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original + @font-face declaration above and find the property called "font-family." The name linked there will be + what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" + property, inside the selector you want to change. For example: +

+ p { font-family: 'WebFont', Arial, sans-serif; } + +

4. Test

+

+ Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to + help you if you find that fonts aren't loading in a particular browser. +

+
+ + +
+
+
+ +
+ + diff --git a/client/app/assets/fonts/uifont/uifont-solid-demo.html b/client/app/assets/fonts/uifont/uifont-solid-demo.html index 58edfc5a7..000d0309a 100644 --- a/client/app/assets/fonts/uifont/uifont-solid-demo.html +++ b/client/app/assets/fonts/uifont/uifont-solid-demo.html @@ -1,477 +1,981 @@ - + - - - - - - - - - - UI Font Solid Regular Specimen - - - - - - -
- - - -
- - -
- -
-
-
AaBb
-
-
- -
-
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
-
-
-
- - - - - - - - - - - - - - - - -
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
- -
- -
- - - -
- - -
-
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
-
- bodyUI Font Solid Regular -
-
- bodyArial -
-
- bodyVerdana -
-
- bodyGeorgia -
- - - -
- - -
- -
-

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
-
-
-

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
- -
- -
- -
-
-

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
-

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
- -
- -
- -
-
-

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
- -
- - - -
-
-

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
- -
-
-

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
-

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

- -
-
- -
- -
-
-

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
-

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
- -
- -
- -
-
-

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

-
-
- -
- - - - -
- -
- -
- -
-

Lorem Ipsum Dolor

-

Etiam porta sem malesuada magna mollis euismod

- - -
-
-
-
-

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

- - -

Pellentesque ornare sem

- -

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

- -

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

- -

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

- -

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

- -

Cras mattis consectetur

- -

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

- -

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

-
- - -
- -
- - - - - - -
-
-
- -

Language Support

-

The subset of UI Font Solid Regular in this kit supports the following languages:
- - English, Arrernte, Bislama, Cebuano, Fijian, Gilbertese, Hmong, Ibanag, Iloko_ilokano, Interglossa_glosa, Interlingua, Lojban, Norfolk_pitcairnese, Oromo, Rotokas, Seychelles_creole, Shona, Somali, Southern_ndebele, Swahili, Swati_swazi, Tok_pisin, Warlpiri, Xhosa, Zulu, Latinbasic, Demo

-

Glyph Chart

-

The subset of UI Font Solid Regular in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.

-
- -

&#32;

-

&#45;

-
-

&#46;

.
-

&#48;

0
-

&#49;

1
-

&#50;

2
-

&#51;

3
-

&#52;

4
-

&#53;

5
-

&#54;

6
-

&#55;

7
-

&#56;

8
-

&#57;

9
-

&#65;

A
-

&#66;

B
-

&#67;

C
-

&#68;

D
-

&#69;

E
-

&#70;

F
-

&#71;

G
-

&#72;

H
-

&#73;

I
-

&#74;

J
-

&#75;

K
-

&#76;

L
-

&#77;

M
-

&#78;

N
-

&#79;

O
-

&#80;

P
-

&#81;

Q
-

&#82;

R
-

&#83;

S
-

&#84;

T
-

&#85;

U
-

&#86;

V
-

&#87;

W
-

&#88;

X
-

&#89;

Y
-

&#90;

Z
-

&#95;

_
-

&#97;

a
-

&#98;

b
-

&#99;

c
-

&#100;

d
-

&#101;

e
-

&#102;

f
-

&#103;

g
-

&#104;

h
-

&#105;

i
-

&#106;

j
-

&#107;

k
-

&#108;

l
-

&#109;

m
-

&#110;

n
-

&#111;

o
-

&#112;

p
-

&#113;

q
-

&#114;

r
-

&#115;

s
-

&#116;

t
-

&#117;

u
-

&#118;

v
-

&#119;

w
-

&#120;

x
-

&#121;

y
-

&#122;

z
-

&#160;

 
-

&#173;

­
-

&#8192;

 
-

&#8193;

-

&#8194;

-

&#8195;

-

&#8196;

-

&#8197;

-

&#8198;

-

&#8199;

-

&#8200;

-

&#8201;

-

&#8202;

-

&#8208;

-

&#8209;

-

&#8210;

-

&#8211;

-

&#8212;

-

&#8239;

-

&#8287;

-

&#9724;

-
-
- - -
-
- - -
- -
- -
-
-
-

Installing Webfonts

- -

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

- -

1. Upload your webfonts

-

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

- -

2. Include the webfont stylesheet

-

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

- - - -@font-face{ - font-family: 'MyWebFont'; - src: url('WebFont.eot'); - src: url('WebFont.eot?#iefix') format('embedded-opentype'), - url('WebFont.woff') format('woff'), - url('WebFont.ttf') format('truetype'), - url('WebFont.svg#webfont') format('svg'); -} - - -

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

- <link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" /> - -

3. Modify your own stylesheet

-

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

-p { font-family: 'WebFont', Arial, sans-serif; } - -

4. Test

-

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

-
- - -
- -
- -
- -
- - \ No newline at end of file + + + + + + + + + + UI Font Solid Regular Specimen + + + + + +
+ + + +
+
+
+
+
AaBb
+
+
+ +
+
+ A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​; +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
10 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
11 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
12 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
13 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
14 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
16 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
18 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
20 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
24 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
30 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
36 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
48 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
60 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
72 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
90 + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ +
+
+
+ +
+
+
+ ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body +
+
body
+
body
+
body
+
+
bodyUI Font Solid Regular
+
bodyArial
+
bodyVerdana
+
bodyGeorgia
+
+ +
+
+

+ 10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+
+
+

+ 14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+ +
+
+

+ 20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+

+ 24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+ +
+
+ +
+
+

+ 30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, + tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh + ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Nulla vitae elit libero, a pharetra augue. +

+
+
+
+
+ +
+
+
+

Lorem Ipsum Dolor

+

Etiam porta sem malesuada magna mollis euismod

+ + +
+
+
+
+

+ Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus + ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +

+ +

Pellentesque ornare sem

+ +

+ Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec + ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. + Nullam id dolor id nibh ultricies vehicula ut id elit. +

+ +

+ Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit + amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. +

+ +

+ Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Aenean lacinia bibendum nulla sed consectetur. +

+ +

+ Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu + leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus + auctor fringilla. +

+ +

Cras mattis consectetur

+ +

+ Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum + nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis + consectetur purus sit amet fermentum. +

+ +

+ Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu + leo. Cras mattis consectetur purus sit amet fermentum. +

+
+ + +
+
+ +
+
+
+

Language Support

+

+ The subset of UI Font Solid Regular in this kit supports the following languages:
+ + English, Arrernte, Bislama, Cebuano, Fijian, Gilbertese, Hmong, Ibanag, Iloko_ilokano, + Interglossa_glosa, Interlingua, Lojban, Norfolk_pitcairnese, Oromo, Rotokas, Seychelles_creole, Shona, + Somali, Southern_ndebele, Swahili, Swati_swazi, Tok_pisin, Warlpiri, Xhosa, Zulu, Latinbasic, Demo +

+

Glyph Chart

+

+ The subset of UI Font Solid Regular in this kit includes all the glyphs listed below. Unicode entities + are included above each glyph to help you insert individual characters into your layout. +

+
+
+

&#32;

+ +
+
+

&#45;

+ - +
+
+

&#46;

+ . +
+
+

&#48;

+ 0 +
+
+

&#49;

+ 1 +
+
+

&#50;

+ 2 +
+
+

&#51;

+ 3 +
+
+

&#52;

+ 4 +
+
+

&#53;

+ 5 +
+
+

&#54;

+ 6 +
+
+

&#55;

+ 7 +
+
+

&#56;

+ 8 +
+
+

&#57;

+ 9 +
+
+

&#65;

+ A +
+
+

&#66;

+ B +
+
+

&#67;

+ C +
+
+

&#68;

+ D +
+
+

&#69;

+ E +
+
+

&#70;

+ F +
+
+

&#71;

+ G +
+
+

&#72;

+ H +
+
+

&#73;

+ I +
+
+

&#74;

+ J +
+
+

&#75;

+ K +
+
+

&#76;

+ L +
+
+

&#77;

+ M +
+
+

&#78;

+ N +
+
+

&#79;

+ O +
+
+

&#80;

+ P +
+
+

&#81;

+ Q +
+
+

&#82;

+ R +
+
+

&#83;

+ S +
+
+

&#84;

+ T +
+
+

&#85;

+ U +
+
+

&#86;

+ V +
+
+

&#87;

+ W +
+
+

&#88;

+ X +
+
+

&#89;

+ Y +
+
+

&#90;

+ Z +
+
+

&#95;

+ _ +
+
+

&#97;

+ a +
+
+

&#98;

+ b +
+
+

&#99;

+ c +
+
+

&#100;

+ d +
+
+

&#101;

+ e +
+
+

&#102;

+ f +
+
+

&#103;

+ g +
+
+

&#104;

+ h +
+
+

&#105;

+ i +
+
+

&#106;

+ j +
+
+

&#107;

+ k +
+
+

&#108;

+ l +
+
+

&#109;

+ m +
+
+

&#110;

+ n +
+
+

&#111;

+ o +
+
+

&#112;

+ p +
+
+

&#113;

+ q +
+
+

&#114;

+ r +
+
+

&#115;

+ s +
+
+

&#116;

+ t +
+
+

&#117;

+ u +
+
+

&#118;

+ v +
+
+

&#119;

+ w +
+
+

&#120;

+ x +
+
+

&#121;

+ y +
+
+

&#122;

+ z +
+
+

&#160;

+   +
+
+

&#173;

+ ­ +
+
+

&#8192;

+   +
+
+

&#8193;

+   +
+
+

&#8194;

+   +
+
+

&#8195;

+   +
+
+

&#8196;

+   +
+
+

&#8197;

+   +
+
+

&#8198;

+   +
+
+

&#8199;

+   +
+
+

&#8200;

+   +
+
+

&#8201;

+   +
+
+

&#8202;

+   +
+
+

&#8208;

+ ‐ +
+
+

&#8209;

+ ‑ +
+
+

&#8210;

+ ‒ +
+
+

&#8211;

+ – +
+
+

&#8212;

+ — +
+
+

&#8239;

+   +
+
+

&#8287;

+   +
+
+

&#9724;

+ ◼ +
+
+
+
+
+ +
+ +
+
+
+

Installing Webfonts

+ +

+ Webfonts are supported by all major browser platforms but not all in the same way. There are currently + four different font formats that must be included in order to target all browsers. This includes TTF, + WOFF, EOT and SVG. +

+ +

1. Upload your webfonts

+

+ You must upload your webfont kit to your website. They should be in or near the same directory as your + CSS files. +

+ +

2. Include the webfont stylesheet

+

+ A special CSS @font-face declaration helps the various browsers select the appropriate font it needs + without causing you a bunch of headaches. Learn more about this syntax by reading the + Fontspring blog post + about it. The code for it is as follows: +

+ + + @font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?#iefix') + format('embedded-opentype'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), + url('WebFont.svg#webfont') format('svg'); } + + +

+ We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in + your HTML, like this: +

+ <link rel="stylesheet" href="stylesheet.css" type="text/css" + charset="utf-8" /> + +

3. Modify your own stylesheet

+

+ To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original + @font-face declaration above and find the property called "font-family." The name linked there will be + what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" + property, inside the selector you want to change. For example: +

+ p { font-family: 'WebFont', Arial, sans-serif; } + +

4. Test

+

+ Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to + help you if you find that fonts aren't loading in a particular browser. +

+
+ + +
+
+
+ +
+ + diff --git a/client/app/assets/images/db-logos/arangodb.png b/client/app/assets/images/db-logos/arangodb.png new file mode 100644 index 000000000..1b2defd2d Binary files /dev/null and b/client/app/assets/images/db-logos/arangodb.png differ diff --git a/client/app/assets/images/db-logos/corporate_memory.png b/client/app/assets/images/db-logos/corporate_memory.png new file mode 100644 index 000000000..f168b02ec Binary files /dev/null and b/client/app/assets/images/db-logos/corporate_memory.png differ diff --git a/client/app/assets/images/db-logos/databend.png b/client/app/assets/images/db-logos/databend.png new file mode 100644 index 000000000..dec146f4c Binary files /dev/null and b/client/app/assets/images/db-logos/databend.png differ diff --git a/client/app/assets/images/db-logos/e6data.png b/client/app/assets/images/db-logos/e6data.png new file mode 100644 index 000000000..af5cf71c4 Binary files /dev/null and b/client/app/assets/images/db-logos/e6data.png differ diff --git a/client/app/assets/images/db-logos/elasticsearch2.png b/client/app/assets/images/db-logos/elasticsearch2.png new file mode 100644 index 000000000..e7cb9c834 Binary files /dev/null and b/client/app/assets/images/db-logos/elasticsearch2.png differ diff --git a/client/app/assets/images/db-logos/elasticsearch2_OpenDistroSQLElasticSearch.png b/client/app/assets/images/db-logos/elasticsearch2_OpenDistroSQLElasticSearch.png new file mode 100644 index 000000000..e7cb9c834 Binary files /dev/null and b/client/app/assets/images/db-logos/elasticsearch2_OpenDistroSQLElasticSearch.png differ diff --git a/client/app/assets/images/db-logos/elasticsearch2_XPackSQLElasticSearch.png b/client/app/assets/images/db-logos/elasticsearch2_XPackSQLElasticSearch.png new file mode 100644 index 000000000..e7cb9c834 Binary files /dev/null and b/client/app/assets/images/db-logos/elasticsearch2_XPackSQLElasticSearch.png differ diff --git a/client/app/assets/images/db-logos/excel.png b/client/app/assets/images/db-logos/excel.png new file mode 100644 index 000000000..001504488 Binary files /dev/null and b/client/app/assets/images/db-logos/excel.png differ diff --git a/client/app/assets/images/db-logos/firebolt.png b/client/app/assets/images/db-logos/firebolt.png new file mode 100644 index 000000000..7b6c02a66 Binary files /dev/null and b/client/app/assets/images/db-logos/firebolt.png differ diff --git a/client/app/assets/images/db-logos/google_analytics4.png b/client/app/assets/images/db-logos/google_analytics4.png new file mode 100644 index 000000000..eaddd9d56 Binary files /dev/null and b/client/app/assets/images/db-logos/google_analytics4.png differ diff --git a/client/app/assets/images/db-logos/google_search_console.png b/client/app/assets/images/db-logos/google_search_console.png new file mode 100644 index 000000000..a302ca590 Binary files /dev/null and b/client/app/assets/images/db-logos/google_search_console.png differ diff --git a/client/app/assets/images/db-logos/ignite.png b/client/app/assets/images/db-logos/ignite.png new file mode 100644 index 000000000..046e38101 Binary files /dev/null and b/client/app/assets/images/db-logos/ignite.png differ diff --git a/client/app/assets/images/db-logos/influxdbv2.png b/client/app/assets/images/db-logos/influxdbv2.png new file mode 100644 index 000000000..f3846cb19 Binary files /dev/null and b/client/app/assets/images/db-logos/influxdbv2.png differ diff --git a/client/app/assets/images/db-logos/nz.png b/client/app/assets/images/db-logos/nz.png new file mode 100644 index 000000000..663687470 Binary files /dev/null and b/client/app/assets/images/db-logos/nz.png differ diff --git a/client/app/assets/images/db-logos/pinot.png b/client/app/assets/images/db-logos/pinot.png new file mode 100644 index 000000000..7527e7b15 Binary files /dev/null and b/client/app/assets/images/db-logos/pinot.png differ diff --git a/client/app/assets/images/db-logos/qubole.png b/client/app/assets/images/db-logos/qubole.png deleted file mode 100644 index dfdc2fa2e..000000000 Binary files a/client/app/assets/images/db-logos/qubole.png and /dev/null differ diff --git a/client/app/assets/images/db-logos/risingwave.png b/client/app/assets/images/db-logos/risingwave.png new file mode 100644 index 000000000..ae4a13f12 Binary files /dev/null and b/client/app/assets/images/db-logos/risingwave.png differ diff --git a/client/app/assets/images/db-logos/sparql_endpoint.png b/client/app/assets/images/db-logos/sparql_endpoint.png new file mode 100644 index 000000000..31ac155d4 Binary files /dev/null and b/client/app/assets/images/db-logos/sparql_endpoint.png differ diff --git a/client/app/assets/images/db-logos/tinybird.png b/client/app/assets/images/db-logos/tinybird.png new file mode 100644 index 000000000..129555e39 Binary files /dev/null and b/client/app/assets/images/db-logos/tinybird.png differ diff --git a/client/app/assets/images/db-logos/trino.png b/client/app/assets/images/db-logos/trino.png new file mode 100644 index 000000000..904db40bb Binary files /dev/null and b/client/app/assets/images/db-logos/trino.png differ diff --git a/client/app/assets/images/db-logos/yandex_disk.png b/client/app/assets/images/db-logos/yandex_disk.png new file mode 100644 index 000000000..7b375648d Binary files /dev/null and b/client/app/assets/images/db-logos/yandex_disk.png differ diff --git a/client/app/assets/images/destinations/asana.png b/client/app/assets/images/destinations/asana.png new file mode 100644 index 000000000..42ea1ab9c Binary files /dev/null and b/client/app/assets/images/destinations/asana.png differ diff --git a/client/app/assets/images/destinations/datadog.png b/client/app/assets/images/destinations/datadog.png new file mode 100644 index 000000000..0c1cd4e58 Binary files /dev/null and b/client/app/assets/images/destinations/datadog.png differ diff --git a/client/app/assets/images/destinations/discord.png b/client/app/assets/images/destinations/discord.png new file mode 100644 index 000000000..0781b84ce Binary files /dev/null and b/client/app/assets/images/destinations/discord.png differ diff --git a/client/app/assets/images/destinations/hipchat.png b/client/app/assets/images/destinations/hipchat.png deleted file mode 100644 index 88ac51210..000000000 Binary files a/client/app/assets/images/destinations/hipchat.png and /dev/null differ diff --git a/client/app/assets/images/destinations/microsoft_teams_webhook.png b/client/app/assets/images/destinations/microsoft_teams_webhook.png new file mode 100644 index 000000000..8ada5c8c6 Binary files /dev/null and b/client/app/assets/images/destinations/microsoft_teams_webhook.png differ diff --git a/client/app/assets/images/destinations/webex.png b/client/app/assets/images/destinations/webex.png new file mode 100644 index 000000000..bea8fd1ca Binary files /dev/null and b/client/app/assets/images/destinations/webex.png differ diff --git a/client/app/assets/images/info.svg b/client/app/assets/images/info.svg new file mode 100644 index 000000000..d0f89b452 --- /dev/null +++ b/client/app/assets/images/info.svg @@ -0,0 +1,5 @@ + + + info + + diff --git a/client/app/assets/less/STYLING-README.md b/client/app/assets/less/STYLING-README.md index fe9841c51..e556d62ee 100644 --- a/client/app/assets/less/STYLING-README.md +++ b/client/app/assets/less/STYLING-README.md @@ -4,5 +4,5 @@ Some general rules before you add stuff. - Avoid using inline css - If possible, use classes that are already in place instead of adding new -- Keep less/inc folder untouched, rewrite things in less/redash respectively +- Keep less/inc folder untouched, rewrite things in less/redash respectively - Try following BEM naming conventions: http://getbem.com/naming/ diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less index 574d420fb..b7acc78d3 100644 --- a/client/app/assets/less/ant.less +++ b/client/app/assets/less/ant.less @@ -16,7 +16,6 @@ @import "~antd/lib/pagination/style/index"; @import "~antd/lib/table/style/index"; @import "~antd/lib/popover/style/index"; -@import "~antd/lib/icon/style/index"; @import "~antd/lib/tag/style/index"; @import "~antd/lib/grid/style/index"; @import "~antd/lib/switch/style/index"; @@ -226,6 +225,16 @@ } } + &-tbody > tr&-row { + &:hover, + &:focus, + &:focus-within { + & > td { + background: @table-row-hover-bg; + } + } + } + // Custom styles &-headerless &-tbody > tr:first-child > td { @@ -392,6 +401,18 @@ left: 0; } } + + &:focus, + &:focus-within { + color: @menu-highlight-color; + } + } +} + +.@{dropdown-prefix-cls}-menu-item { + &:focus, + &:focus-within { + background-color: @item-hover-bg; } } @@ -402,3 +423,18 @@ .@{checkbox-prefix-cls} + span { padding-right: 0; } + +// make sure Multiple select has room for icons +.@{select-prefix-cls}-multiple { + &.@{select-prefix-cls}-show-arrow, + &.@{select-prefix-cls}-show-search, + &.@{select-prefix-cls}-loading { + .@{select-prefix-cls}-selector { + padding-right: 30px; + } + } +} + +.ant-btn-icon-only.ant-btn-sm { + width: auto !important; +} diff --git a/client/app/assets/less/icons.less b/client/app/assets/less/icons.less index c87a2fe8f..ea6a1e8f0 100644 --- a/client/app/assets/less/icons.less +++ b/client/app/assets/less/icons.less @@ -1,19 +1,20 @@ @font-face { - font-family: 'icomoon'; - src: url('../fonts/icomoon.eot?x3z2b6'); - src: url('../fonts/icomoon.eot?x3z2b6#iefix') format('embedded-opentype'), - url('../fonts/icomoon.ttf?x3z2b6') format('truetype'), - url('../fonts/icomoon.woff?x3z2b6') format('woff'), - url('../fonts/icomoon.svg?x3z2b6#icomoon') format('svg'); + font-family: "icomoon"; + src: url("../fonts/icomoon.eot?x3z2b6"); + src: + url("../fonts/icomoon.eot?x3z2b6#iefix") format("embedded-opentype"), + url("../fonts/icomoon.ttf?x3z2b6") format("truetype"), + url("../fonts/icomoon.woff?x3z2b6") format("woff"), + url("../fonts/icomoon.svg?x3z2b6#icomoon") format("svg"); font-weight: normal; font-style: normal; font-display: block; } -[class^="icon-"], [class*=" icon-"] { +[class^="icon-"], +[class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'icomoon' !important; - speak: never; + font-family: "icomoon" !important; font-style: normal; font-weight: normal; font-variant: normal; diff --git a/client/app/assets/less/inc/404.less b/client/app/assets/less/inc/404.less index 87e393581..9124267ad 100755 --- a/client/app/assets/less/inc/404.less +++ b/client/app/assets/less/inc/404.less @@ -1,73 +1,68 @@ -.four-zero { - background: @white; - box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); - border-radius: 2px; - position: absolute; - top: 50%; - margin-top: -150px; - text-align: center; - padding: 15px; - height: 300px; - width: 500px; - left: 50%; - color: #333; - margin-left: -250px; - - h2 { - font-size: 130px; - } - - - @media (max-width: @screen-xs-max) { - width: ~"calc(100% - 40px)"; - left: 20px; - margin-left: 0; - height: 260px; - margin-top: -130px; - - h2 { - font-size: 90px; - } - } - - h2 { - line-height: 100%; - font-weight: 100; - } - - small { - display: block; - font-size: 26px; - margin-top: -10px - } - - - footer { - background: @ace; - position: absolute; - left: 0; - bottom: 0; - width: 100%; - padding: 10px; - - & > a { - font-size: 21px; - display: inline-block; - color: #333; - margin: 0 1px; - line-height: 40px; - width: 40px; - height: 40px; - background: rgba(0, 0, 0, 0.09); - border-radius: 50%; - text-align: center; - - &:hover { - background: rgba(0, 0, 0, 0.2); - } - } - } -} - - - +.four-zero { + background: @white; + box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); + border-radius: 2px; + position: absolute; + top: 50%; + margin-top: -150px; + text-align: center; + padding: 15px; + height: 300px; + width: 500px; + left: 50%; + color: #333; + margin-left: -250px; + + h2 { + font-size: 130px; + } + + @media (max-width: @screen-xs-max) { + width: ~"calc(100% - 40px)"; + left: 20px; + margin-left: 0; + height: 260px; + margin-top: -130px; + + h2 { + font-size: 90px; + } + } + + h2 { + line-height: 100%; + font-weight: 100; + } + + small { + display: block; + font-size: 26px; + margin-top: -10px; + } + + footer { + background: @ace; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + padding: 10px; + + & > a { + font-size: 21px; + display: inline-block; + color: #333; + margin: 0 1px; + line-height: 40px; + width: 40px; + height: 40px; + background: rgba(0, 0, 0, 0.09); + border-radius: 50%; + text-align: center; + + &:hover { + background: rgba(0, 0, 0, 0.2); + } + } + } +} diff --git a/client/app/assets/less/inc/alert.less b/client/app/assets/less/inc/alert.less index 3e73d9b54..8766ea3e0 100755 --- a/client/app/assets/less/inc/alert.less +++ b/client/app/assets/less/inc/alert.less @@ -1,45 +1,49 @@ -.alert-page h3 { - flex-grow: 1; - - input { - margin: -0.2em 0; - width: 100%; - min-width: 170px; - } -} - -.btn-create-alert[disabled] { - display: block; - margin-top: -20px; -} - -.alert-state { - border-bottom: 1px solid @input-border; - padding-bottom: 30px; - - .alert-state-indicator { - text-transform: uppercase; - font-size: 14px; - padding: 5px 8px; - } - - .alert-last-triggered { - color: @headings-color; - } -} - -.alert-query-selector { - min-width: 250px; - width: auto !important; -} - -// allow form item labels to gracefully break line -.alert-form-item label { - white-space: initial; - padding-right: 8px; - line-height: 21px; - - &::after { - margin-right: 0 !important; - } -} +.alert-page h3 { + flex-grow: 1; + + input { + margin: -0.2em 0; + width: 100%; + min-width: 170px; + } +} + +.btn-create-alert[disabled] { + display: block; + margin-top: -20px; +} + +.alert-state { + border-bottom: 1px solid @input-border; + padding-bottom: 30px; + + .alert-state-indicator { + text-transform: uppercase; + font-size: 14px; + padding: 5px 8px; + } + + .ant-form-item-explain { + margin-top: 10px; + } + + .alert-last-triggered { + color: @headings-color; + } +} + +.alert-query-selector { + min-width: 250px; + width: auto !important; +} + +// allow form item labels to gracefully break line +.alert-form-item label { + white-space: initial; + padding-right: 8px; + line-height: 21px; + + &::after { + margin-right: 0 !important; + } +} diff --git a/client/app/assets/less/inc/base.less b/client/app/assets/less/inc/base.less index 4bc56bdd9..fbae022bd 100755 --- a/client/app/assets/less/inc/base.less +++ b/client/app/assets/less/inc/base.less @@ -111,7 +111,9 @@ strong { .resize-both, .resize-vertical.resize-horizontal { resize: both !important; - transition: height 0s, width 0s !important; + transition: + height 0s, + width 0s !important; } .bg-ace { diff --git a/client/app/assets/less/inc/bootstrap-overrides.less b/client/app/assets/less/inc/bootstrap-overrides.less index 643a7e67d..49d9340dc 100755 --- a/client/app/assets/less/inc/bootstrap-overrides.less +++ b/client/app/assets/less/inc/bootstrap-overrides.less @@ -1,49 +1,49 @@ /** Media - Overriding the Media object to 3.2 version in order to prevent issues like text overflow. **/ .media { - margin-top: 0; - .clearfix(); + margin-top: 0; + .clearfix(); - & > .pull-left { - padding-right: 15px; - } + & > .pull-left { + padding-right: 15px; + } - & > .pull-right { - padding-left: 15px; - } + & > .pull-right { + padding-left: 15px; + } - overflow: visible; + overflow: visible; } .media-heading { - font-size: 14px; - margin-bottom: 10px; + font-size: 14px; + margin-bottom: 10px; } .media-body { - zoom: 1; - display: block; - width: auto; + zoom: 1; + display: block; + width: auto; } .media-object { - border-radius: 2px; + border-radius: 2px; } .collapsing, .collapse.in { - padding: 0; - transition: all 0.35s ease; + padding: 0; + transition: all 0.35s ease; } /** LIST **/ .list-inline > li { - vertical-align: top; - margin-left: 0; + vertical-align: top; + margin-left: 0; } // Hide URLs next to links when printing (override `bootstrap` rules) @media print { - a[href]:after { - content: none !important; - } + a[href]:after { + content: none !important; + } } diff --git a/client/app/assets/less/inc/breadcrumb.less b/client/app/assets/less/inc/breadcrumb.less index 502d8038e..15a603c7a 100755 --- a/client/app/assets/less/inc/breadcrumb.less +++ b/client/app/assets/less/inc/breadcrumb.less @@ -1,29 +1,29 @@ -.breadcrumb { - border-bottom: 1px solid #E5E5E5; - border-radius: 0; - padding-top: 10px; - padding-right: 33px; - padding-bottom: 11px; - - @media (min-width: (@screen-lg-min + 80px)) { - padding-left: (@sidebar-left-width + @grid-gutter-width); - } - - @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { - padding-left: (@sidebar-left-mid-width + @grid-gutter-width); - } - - @media (max-width: (@screen-sm-min)) { - padding-left: @grid-gutter-width/2; - } - - & > li { - & > a { - color: #A9A9A9; - - &:hover { - color: @breadcrumb-active-color; - } - } - } -} +.breadcrumb { + border-bottom: 1px solid #e5e5e5; + border-radius: 0; + padding-top: 10px; + padding-right: 33px; + padding-bottom: 11px; + + @media (min-width: (@screen-lg-min + 80px)) { + padding-left: (@sidebar-left-width + @grid-gutter-width); + } + + @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { + padding-left: (@sidebar-left-mid-width + @grid-gutter-width); + } + + @media (max-width: (@screen-sm-min)) { + padding-left: @grid-gutter-width / 2; + } + + & > li { + & > a { + color: #a9a9a9; + + &:hover { + color: @breadcrumb-active-color; + } + } + } +} diff --git a/client/app/assets/less/inc/button.less b/client/app/assets/less/inc/button.less index d6661f977..79641f0a0 100755 --- a/client/app/assets/less/inc/button.less +++ b/client/app/assets/less/inc/button.less @@ -1,142 +1,154 @@ -.btn { - &:not(.btn-alt) { - border: 0; - } - - &[class*="bg-"]:not(.bg-white) { - color: #fff; - } - - .caret { - margin-top: -3px; - } - - &:not(.btn-link) { - &:active, - &.active, - &:hover { - - } - } -} - -.btn-default { - .button-variant(#333, #eee, transparent); -} - -.btn-inverse { - .button-variant(#fff, #454545, transparent); -} - -.btn-link { - color: #333; -} - -.btn-icon { - border-radius: 50%; - width: 40px; - height: 40px; - padding: 0; - text-align: center; - - .zmdi { - font-size: 17px; - } -} - -.btn-icon-text { - & > .zmdi { - font-size: 15px; - vertical-align: top; - display: inline-block; - margin-top: 2px; - line-height: 100%; - margin-right: 5px; - } -} - -.open .btn { - outline: none !important; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important; - - &:focus, &:active { - outline: none !important; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important; - } -} - -/** ALTERNATIVE BUTTONS **/ -.btn-alt(@color) { - border-color: @color; - color: @color; - - &:not(.btn-white) { - &:hover, - &:active, - &:focus { - color: #fff; - background: @color; - } - } - - &.btn-white { - &:hover, - &:active, - &:focus { - color: #333; - background: @color; - } - } -} - -.btn-alt { - background: transparent; - - &.btn-default { - .btn-alt(darken(@brand-default, 30%)); - } - - &.btn-info { - .btn-alt(@brand-info); - } - - &.btn-primary { - .btn-alt(@brand-primary); - } - - &.btn-success { - .btn-alt(@brand-success); - } - - &.btn-warning { - .btn-alt(@brand-warning); - } - - &.btn-danger { - .btn-alt(@brand-danger); - } -} - -.btn-xs > .fa { - font-size: 14px; - top: 1px; - position: relative; -} - - -.btn-default { - background-color: fade(@redash-gray, 15%); -} - -.btn-transparent { - background-color: transparent !important; -} - -.btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { - background-color: fade(@redash-gray, 25%); -} - -.btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus { - color: #333; - background-color: fade(@redash-gray, 45%); -} \ No newline at end of file +.btn { + &:not(.btn-alt) { + border: 0; + } + + &[class*="bg-"]:not(.bg-white) { + color: #fff; + } + + .caret { + margin-top: -3px; + } + + &:not(.btn-link) { + &:active, + &.active, + &:hover { + } + } +} + +.btn-default { + .button-variant(#333, #eee, transparent); +} + +.btn-inverse { + .button-variant(#fff, #454545, transparent); +} + +.btn-link { + color: #333; +} + +.btn-icon { + border-radius: 50%; + width: 40px; + height: 40px; + padding: 0; + text-align: center; + + .zmdi { + font-size: 17px; + } +} + +.btn-icon-text { + & > .zmdi { + font-size: 15px; + vertical-align: top; + display: inline-block; + margin-top: 2px; + line-height: 100%; + margin-right: 5px; + } +} + +.open .btn { + outline: none !important; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important; + + &:focus, + &:active { + outline: none !important; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important; + } +} + +/** ALTERNATIVE BUTTONS **/ +.btn-alt(@color) { + border-color: @color; + color: @color; + + &:not(.btn-white) { + &:hover, + &:active, + &:focus { + color: #fff; + background: @color; + } + } + + &.btn-white { + &:hover, + &:active, + &:focus { + color: #333; + background: @color; + } + } +} + +.btn-alt { + background: transparent; + + &.btn-default { + .btn-alt(darken(@brand-default, 30%)); + } + + &.btn-info { + .btn-alt(@brand-info); + } + + &.btn-primary { + .btn-alt(@brand-primary); + } + + &.btn-success { + .btn-alt(@brand-success); + } + + &.btn-warning { + .btn-alt(@brand-warning); + } + + &.btn-danger { + .btn-alt(@brand-danger); + } +} + +.btn-xs > .fa { + font-size: 14px; + top: 1px; + position: relative; +} + +.btn-default { + background-color: fade(@redash-gray, 15%); +} + +.btn-transparent { + background-color: transparent !important; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-color: fade(@redash-gray, 25%); +} + +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: fade(@redash-gray, 45%); +} diff --git a/client/app/assets/less/inc/carousel.less b/client/app/assets/less/inc/carousel.less index 129c18437..d6703ed4e 100755 --- a/client/app/assets/less/inc/carousel.less +++ b/client/app/assets/less/inc/carousel.less @@ -1,41 +1,42 @@ -.carousel-caption { - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.6); - - h3 { - margin-top: 0; - margin-bottom: 3px; - color: #fff; - } -} - -.carousel-indicators { - bottom: 10px; - - & > li:not(.active) { - border: 0; - background: #000; - } -} - -.carousel-control { - width: 50px; - background: none; - - .fa { - font-size: 50px; - height: 52px; - margin-top: -26px; - position: absolute; - top: 50%; - .margin-left(-9px); - } -} - -@media @max-768 { - .carousel-indicators, .carousel-caption { - display: none; - } -} \ No newline at end of file +.carousel-caption { + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + + h3 { + margin-top: 0; + margin-bottom: 3px; + color: #fff; + } +} + +.carousel-indicators { + bottom: 10px; + + & > li:not(.active) { + border: 0; + background: #000; + } +} + +.carousel-control { + width: 50px; + background: none; + + .fa { + font-size: 50px; + height: 52px; + margin-top: -26px; + position: absolute; + top: 50%; + .margin-left(-9px); + } +} + +@media @max-768 { + .carousel-indicators, + .carousel-caption { + display: none; + } +} diff --git a/client/app/assets/less/inc/chart.less b/client/app/assets/less/inc/chart.less index 51fadbc78..20862ba9f 100755 --- a/client/app/assets/less/inc/chart.less +++ b/client/app/assets/less/inc/chart.less @@ -1,237 +1,235 @@ -/* -------------------------------------------------------- - Chart Helper Classes ------------------------------------------------------------*/ -.main-chart { - margin: 0px -8px 0 -10px; - overflow: hidden; - position: relative; - bottom: -10px; -} - -.mc-item { - width: 100%; - height: 250px; -} - -.mc-pie { - width: 100%; - height: 300px; -} - -@media (min-width: @screen-sm-min) { - .mc-info { - position: absolute; - bottom: 10px; - z-index: 1; - padding: 10px 20px 15px; - left: 10px; - background-color: rgba(0, 150, 136, 0.55); - color: #fff; - max-width: 270px; - - span { - font-size: 33px; - } - - small { - display: block; - margin-top: -3px; - margin-left: 3px; - line-height: 130%; - } - } -} - -/* -------------------------------------------------------- - Overview Small Charts ------------------------------------------------------------*/ -.o-item { - padding: 0 20px 15px 20px; - color: #fff; - margin-bottom: @grid-gutter-width; - box-shadow: @tile-shadow; -} - -.oi-number { - font-size: 23px; - display: block; - margin-top: 6px; - margin-bottom: 3px; - line-height: 100%; -} - -.oi-title { - text-transform: uppercase; - .text-overflow(); - line-height: 100%; - padding: 10px 15px; - width: auto; - margin: 0 -21px 20px; -} - - -/* -------------------------------------------------------- - Count Box ------------------------------------------------------------*/ -.count-box { - padding: 20px 23px 0; - - [class*="col-"] { - padding-left: 8px; - padding-right: 8px; - } -} - -.cb-item { - background: rgba(255,255,255,0.22); - padding: 10px 0; - text-align: center; - margin-bottom: 16px; - - & > h3 { - margin: 0; - line-height: 100%; - color: #fff; - font-weight: normal; - } - - & > small { - line-height: 100%; - margin-top: 1px; - display: block; - font-size: 11px; - color: #fff; - } -} - - -/* -------------------------------------------------------- - Flot Charts ------------------------------------------------------------*/ -.flot-legend { - text-align: center; - margin: 10px 0 5px; - - table { - display: inline-block; - } - - .legendColorBox { - & > div { - border: #fff !important; - - & > div { - border-radius: 50%; - } - } - } - - .legendLabel { - padding: 0 8px 0 3px; - } -} - -[class*="flc-"] { - text-align: center; - margin: 20px 0 5px; - - table { - display: inline-block; - } - - .legendColorBox { - & > div { - border: #fff !important; - - & > div { - border-radius: 50%; - } - } - } - - .legendLabel { - padding: 0 8px 0 3px; - } -} - -/* -------------------------------------------------------- - Easy Pie Charts ------------------------------------------------------------*/ -.pie-overviews { - margin-bottom: -15px; -} - -.po-item { - display: inline-block; - position: relative; - margin: 0 5px 10px; - padding-bottom: 13px; - color: #fff; -} - -.poi-percent { - position: absolute; - text-align: center; - width: 100%; - margin-top: 32px; - font-size: 27px; - text-shadow: none; - padding-left: 2px; - - &:after { - content: '%'; - font-size: 11px; - } -} - -.poi-title { - position: absolute; - bottom: 0; - width: 100%; - text-align: center; - font-size: 12px; - - i { - font-size: 15px; - font-weight: normal; - -webkit-font-smoothing: antialiased; - .opacity(0.5); - - &:hover { - .opacity(1); - cursor: pointer; - } - } -} - -/* -------------------------------------------------------- - Chart Tooltips ------------------------------------------------------------*/ -#jqstooltip, -.chart-tooltip { - min-width: 21px; - min-height: 23px; - text-align: center; - border: 0; - background: #333; -} - -#jqstooltip .jqsfield, -.chart-tooltip { - font-size: 12px; - font-weight: 500; - font-family: inherit; - text-align: center; - color: #fff; -} - -#jqstooltip .jqsfield { - & > span { - display: none; - } -} - -.chart-tooltip { - position: absolute; - padding: 6px 10px 5px; -} \ No newline at end of file +/* -------------------------------------------------------- + Chart Helper Classes +-----------------------------------------------------------*/ +.main-chart { + margin: 0px -8px 0 -10px; + overflow: hidden; + position: relative; + bottom: -10px; +} + +.mc-item { + width: 100%; + height: 250px; +} + +.mc-pie { + width: 100%; + height: 300px; +} + +@media (min-width: @screen-sm-min) { + .mc-info { + position: absolute; + bottom: 10px; + z-index: 1; + padding: 10px 20px 15px; + left: 10px; + background-color: rgba(0, 150, 136, 0.55); + color: #fff; + max-width: 270px; + + span { + font-size: 33px; + } + + small { + display: block; + margin-top: -3px; + margin-left: 3px; + line-height: 130%; + } + } +} + +/* -------------------------------------------------------- + Overview Small Charts +-----------------------------------------------------------*/ +.o-item { + padding: 0 20px 15px 20px; + color: #fff; + margin-bottom: @grid-gutter-width; + box-shadow: @tile-shadow; +} + +.oi-number { + font-size: 23px; + display: block; + margin-top: 6px; + margin-bottom: 3px; + line-height: 100%; +} + +.oi-title { + text-transform: uppercase; + .text-overflow(); + line-height: 100%; + padding: 10px 15px; + width: auto; + margin: 0 -21px 20px; +} + +/* -------------------------------------------------------- + Count Box +-----------------------------------------------------------*/ +.count-box { + padding: 20px 23px 0; + + [class*="col-"] { + padding-left: 8px; + padding-right: 8px; + } +} + +.cb-item { + background: rgba(255, 255, 255, 0.22); + padding: 10px 0; + text-align: center; + margin-bottom: 16px; + + & > h3 { + margin: 0; + line-height: 100%; + color: #fff; + font-weight: normal; + } + + & > small { + line-height: 100%; + margin-top: 1px; + display: block; + font-size: 11px; + color: #fff; + } +} + +/* -------------------------------------------------------- + Flot Charts +-----------------------------------------------------------*/ +.flot-legend { + text-align: center; + margin: 10px 0 5px; + + table { + display: inline-block; + } + + .legendColorBox { + & > div { + border: #fff !important; + + & > div { + border-radius: 50%; + } + } + } + + .legendLabel { + padding: 0 8px 0 3px; + } +} + +[class*="flc-"] { + text-align: center; + margin: 20px 0 5px; + + table { + display: inline-block; + } + + .legendColorBox { + & > div { + border: #fff !important; + + & > div { + border-radius: 50%; + } + } + } + + .legendLabel { + padding: 0 8px 0 3px; + } +} + +/* -------------------------------------------------------- + Easy Pie Charts +-----------------------------------------------------------*/ +.pie-overviews { + margin-bottom: -15px; +} + +.po-item { + display: inline-block; + position: relative; + margin: 0 5px 10px; + padding-bottom: 13px; + color: #fff; +} + +.poi-percent { + position: absolute; + text-align: center; + width: 100%; + margin-top: 32px; + font-size: 27px; + text-shadow: none; + padding-left: 2px; + + &:after { + content: "%"; + font-size: 11px; + } +} + +.poi-title { + position: absolute; + bottom: 0; + width: 100%; + text-align: center; + font-size: 12px; + + i { + font-size: 15px; + font-weight: normal; + -webkit-font-smoothing: antialiased; + .opacity(0.5); + + &:hover { + .opacity(1); + cursor: pointer; + } + } +} + +/* -------------------------------------------------------- + Chart Tooltips +-----------------------------------------------------------*/ +#jqstooltip, +.chart-tooltip { + min-width: 21px; + min-height: 23px; + text-align: center; + border: 0; + background: #333; +} + +#jqstooltip .jqsfield, +.chart-tooltip { + font-size: 12px; + font-weight: 500; + font-family: inherit; + text-align: center; + color: #fff; +} + +#jqstooltip .jqsfield { + & > span { + display: none; + } +} + +.chart-tooltip { + position: absolute; + padding: 6px 10px 5px; +} diff --git a/client/app/assets/less/inc/dropdown.less b/client/app/assets/less/inc/dropdown.less index 09e462fb9..6874cc2f9 100755 --- a/client/app/assets/less/inc/dropdown.less +++ b/client/app/assets/less/inc/dropdown.less @@ -1,82 +1,81 @@ - .dropdown-menu { - z-index: 1000000000; - box-shadow: @dropdown-shadow; - margin-top: 1px; - border-width: 0; - display: block; + z-index: 1000000000; + box-shadow: @dropdown-shadow; + margin-top: 1px; + border-width: 0; + display: block; - > .disabled{ - cursor: not-allowed; - // The real magic ;) - > a { - pointer-events: none; - color: @dropdown-link-disabled-color; - } + > .disabled { + cursor: not-allowed; + // The real magic ;) + > a { + pointer-events: none; + color: @dropdown-link-disabled-color; } + } - & > li > a { - padding: 8px 17px; - } + & > li > a { + padding: 8px 17px; + } - &.dm-icon { - & > li > a > .zmdi { - line-height: 100%; - vertical-align: top; - font-size: 18px; - width: 28px; - } + &.dm-icon { + & > li > a > .zmdi { + line-height: 100%; + vertical-align: top; + font-size: 18px; + width: 28px; } + } - &:not([class*="bg-"]) { - & > li > a { - &:hover { - color: #000; - } - } + &:not([class*="bg-"]) { + & > li > a { + &:hover { + color: #000; + } } + } - &[class*="bg-"] { - & > li > a { - font-weight: 300; - color: #fff; - } + &[class*="bg-"] { + & > li > a { + font-weight: 300; + color: #fff; } + } } .dropdown-header { - padding: 10px 15px 9px; - text-transform: uppercase; - font-weight: normal; - border-radius: 1px 1px 0 0; - line-height: 100%; - border-radius: 2px 2px 0 0; + padding: 10px 15px 9px; + text-transform: uppercase; + font-weight: normal; + border-radius: 1px 1px 0 0; + line-height: 100%; + border-radius: 2px 2px 0 0; - &[class*="bg-"] { - color: #fff; - } + &[class*="bg-"] { + color: #fff; + } - .actions { - top: 0; - right: 0; + .actions { + top: 0; + right: 0; - & > li > a { - display: block; - padding: 6px 0 5px; - width: 33px; - text-align: center; + & > li > a { + display: block; + padding: 6px 0 5px; + width: 33px; + text-align: center; - &:hover { - background: rgba(0,0,0,0.08); - } - } + &:hover { + background: rgba(0, 0, 0, 0.08); + } } + } } .dropdown-menu { - >span { - >li { - >a { + > span { + > li { + > a { display: block; padding: 3px 20px; clear: both; @@ -84,7 +83,8 @@ line-height: 1.428571429; color: #333333; white-space: nowrap; - &:hover, &:focus { + &:hover, + &:focus { color: #ffffff; text-decoration: none; background-color: #428bca; @@ -93,4 +93,3 @@ } } } - diff --git a/client/app/assets/less/inc/flex.less b/client/app/assets/less/inc/flex.less index 8911a60fe..b03ab0a03 100644 --- a/client/app/assets/less/inc/flex.less +++ b/client/app/assets/less/inc/flex.less @@ -1,38 +1,102 @@ -.d-flex { display: flex !important; } -.d-inline-flex { display: inline-flex !important; } +.d-flex { + display: flex !important; +} +.d-inline-flex { + display: inline-flex !important; +} -.flex-row { flex-direction: row !important; } -.flex-column { flex-direction: column !important; } -.flex-row-reverse { flex-direction: row-reverse !important; } -.flex-column-reverse { flex-direction: column-reverse !important; } +.flex-row { + flex-direction: row !important; +} +.flex-column { + flex-direction: column !important; +} +.flex-row-reverse { + flex-direction: row-reverse !important; +} +.flex-column-reverse { + flex-direction: column-reverse !important; +} -.flex-wrap { flex-wrap: wrap !important; } -.flex-nowrap { flex-wrap: nowrap !important; } -.flex-wrap-reverse { flex-wrap: wrap-reverse !important; } -.flex-fill { flex: 1 1 auto !important; } +.flex-wrap { + flex-wrap: wrap !important; +} +.flex-nowrap { + flex-wrap: nowrap !important; +} +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} +.flex-fill { + flex: 1 1 auto !important; +} -.justify-content-start { justify-content: flex-start !important; } -.justify-content-end { justify-content: flex-end !important; } -.justify-content-center { justify-content: center !important; } -.justify-content-between { justify-content: space-between !important; } -.justify-content-around { justify-content: space-around !important; } +.justify-content-start { + justify-content: flex-start !important; +} +.justify-content-end { + justify-content: flex-end !important; +} +.justify-content-center { + justify-content: center !important; +} +.justify-content-between { + justify-content: space-between !important; +} +.justify-content-around { + justify-content: space-around !important; +} -.align-items-start { align-items: flex-start !important; } -.align-items-end { align-items: flex-end !important; } -.align-items-center { align-items: center !important; } -.align-items-baseline { align-items: baseline !important; } -.align-items-stretch { align-items: stretch !important; } +.align-items-start { + align-items: flex-start !important; +} +.align-items-end { + align-items: flex-end !important; +} +.align-items-center { + align-items: center !important; +} +.align-items-baseline { + align-items: baseline !important; +} +.align-items-stretch { + align-items: stretch !important; +} -.align-content-start { align-content: flex-start !important; } -.align-content-end { align-content: flex-end !important; } -.align-content-center { align-content: center !important; } -.align-content-between { align-content: space-between !important; } -.align-content-around { align-content: space-around !important; } -.align-content-stretch { align-content: stretch !important; } +.align-content-start { + align-content: flex-start !important; +} +.align-content-end { + align-content: flex-end !important; +} +.align-content-center { + align-content: center !important; +} +.align-content-between { + align-content: space-between !important; +} +.align-content-around { + align-content: space-around !important; +} +.align-content-stretch { + align-content: stretch !important; +} -.align-self-auto { align-self: auto !important; } -.align-self-start { align-self: flex-start !important; } -.align-self-end { align-self: flex-end !important; } -.align-self-center { align-self: center !important; } -.align-self-baseline { align-self: baseline !important; } -.align-self-stretch { align-self: stretch !important; } +.align-self-auto { + align-self: auto !important; +} +.align-self-start { + align-self: flex-start !important; +} +.align-self-end { + align-self: flex-end !important; +} +.align-self-center { + align-self: center !important; +} +.align-self-baseline { + align-self: baseline !important; +} +.align-self-stretch { + align-self: stretch !important; +} diff --git a/client/app/assets/less/inc/form.less b/client/app/assets/less/inc/form.less index 07859d5e3..e68f92dab 100755 --- a/client/app/assets/less/inc/form.less +++ b/client/app/assets/less/inc/form.less @@ -173,7 +173,9 @@ textarea.v-resizable { &:active { &:before { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px fade(@color, 20%); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.28), + 0 0 0 20px fade(@color, 20%); } } } @@ -217,9 +219,13 @@ textarea.v-resizable { background: #fafafa; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); border-radius: 50%; - webkit-transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), + webkit-transition: + left 0.28s cubic-bezier(0.4, 0, 0.2, 1), + background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); - transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), + transition: + left 0.28s cubic-bezier(0.4, 0, 0.2, 1), + background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); } } @@ -228,7 +234,9 @@ textarea.v-resizable { .ts-helper { &:active { &:before { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px rgba(128, 128, 128, 0.1); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.28), + 0 0 0 20px rgba(128, 128, 128, 0.1); } } } diff --git a/client/app/assets/less/inc/generics.less b/client/app/assets/less/inc/generics.less index d7f484da0..e23481deb 100755 --- a/client/app/assets/less/inc/generics.less +++ b/client/app/assets/less/inc/generics.less @@ -1,164 +1,228 @@ -/* -------------------------------------------------------- - Generate Margin Classes (0px - 25px) - margin, margin-top, margin-bottom, margin-left, margin-right ------------------------------------------------------------*/ -.margin (@label, @size: 1, @key:1) when (@size =< 30){ - .m-@{key} { - margin: @size !important; - } - - .m-t-@{key} { - margin-top: @size !important; - } - - .m-b-@{key} { - margin-bottom: @size !important; - } - - .m-l-@{key} { - margin-left: @size !important; - } - - .m-r-@{key} { - margin-right: @size !important; - } - - .margin(@label - 5; @size + 5; @key + 5); -} - -.margin(25, 0px, 0); - -.m-2{ - margin:2px; -} - -/* -------------------------------------------------------- - Generate Padding Classes (0px - 25px) - padding, padding-top, padding-bottom, padding-left, padding-right ------------------------------------------------------------*/ -.padding (@label, @size: 1, @key:1) when (@size =< 30){ - .p-@{key} { - padding: @size !important; - } - - .p-t-@{key} { - padding-top: @size !important; - } - - .p-b-@{key} { - padding-bottom: @size !important; - } - - .p-l-@{key} { - padding-left: @size !important; - } - - .p-r-@{key} { - padding-right: @size !important; - } - - .padding(@label - 5; @size + 5; @key + 5); -} - -.padding(25, 0px, 0); - - -/* -------------------------------------------------------- - Generate Font-Size Classes (8px - 20px) ------------------------------------------------------------*/ -.font-size (@label, @size: 8, @key:10) when (@size =< 20){ - .f-@{key} { - font-size: @size !important; - } - - .font-size(@label - 1; @size + 1; @key + 1); -} - -.font-size(20, 8px, 8); - -.f-inherit { font-size: inherit !important; } - - -/* -------------------------------------------------------- - Font Weight ------------------------------------------------------------*/ -.f-300 { font-weight: 300 !important; } -.f-400 { font-weight: 400 !important; } -.f-500 { font-weight: 500 !important; } -.f-700 { font-weight: 700 !important; } - - -/* -------------------------------------------------------- - Position ------------------------------------------------------------*/ -.p-relative { position: relative !important; } -.p-absolute { position: absolute !important; } -.p-fixed { position: fixed !important; } -.p-static { position: static !important; } - - -/* -------------------------------------------------------- - Overflow ------------------------------------------------------------*/ -.o-hidden { overflow: hidden !important; } -.o-visible { overflow: visible !important; } -.o-auto { overflow: auto !important; } - - -/* -------------------------------------------------------- - Display ------------------------------------------------------------*/ -.di-block { display: inline-block !important; } -.d-block { display: block; } - -/* -------------------------------------------------------- - Background Colors and Colors ------------------------------------------------------------*/ -@array: c-white bg-white @white, c-ace bg-ace @ace, c-black bg-black @black, c-brown bg-brown @brown, c-pink bg-pink @pink, c-red bg-red @red, c-blue bg-blue @blue, c-purple bg-purple @purple, c-deeppurple bg-deeppurple @deeppurple, c-lightblue bg-lightblue @lightblue, c-cyan bg-cyan @cyan, c-teal bg-teal @teal, c-green bg-green @green, c-lightgreen bg-lightgreen @lightgreen, c-lime bg-lime @lime, c-yellow bg-yellow @yellow, c-amber bg-amber @amber, c-orange bg-orange @orange, c-deeporange bg-deeporange @deeporange, c-gray bg-gray @gray, c-bluegray bg-bluegray @bluegray, c-indigo bg-indigo @indigo; - -.for(@array); .-each(@value) { - @name: extract(@value, 1); - @name2: extract(@value, 2); - @color: extract(@value, 3); - &.@{name2} { - background-color: @color !important; - } - - &.@{name} { - color: @color !important; - } -} - - -/* -------------------------------------------------------- - Background Colors ------------------------------------------------------------*/ -.bg-brand { background-color: @brand-bg; } -.bg-black-trp { background-color: rgba(0,0,0,0.12) !important; } - - - -/* -------------------------------------------------------- - Borders ------------------------------------------------------------*/ -.b-0 { border: 0 !important; } - - -/* -------------------------------------------------------- - Width ------------------------------------------------------------*/ -.w-100 { width: 100% !important; } -.w-50 { width: 50% !important; } -.w-25 { width: 25% !important; } - - -/* -------------------------------------------------------- - Border Radius ------------------------------------------------------------*/ -.brd-2 { border-radius: 2px; } - - -/* -------------------------------------------------------- - Alignment ------------------------------------------------------------*/ -.va-top { vertical-align: top; } \ No newline at end of file +/* -------------------------------------------------------- + Generate Margin Classes (0px - 25px) + margin, margin-top, margin-bottom, margin-left, margin-right +-----------------------------------------------------------*/ +.margin (@label, @size: 1, @key:1) when (@size =< 30) { + .m-@{key} { + margin: @size !important; + } + + .m-t-@{key} { + margin-top: @size !important; + } + + .m-b-@{key} { + margin-bottom: @size !important; + } + + .m-l-@{key} { + margin-left: @size !important; + } + + .m-r-@{key} { + margin-right: @size !important; + } + + .margin(@label - 5; @size + 5; @key + 5); +} + +.margin(25, 0px, 0); + +.m-2 { + margin: 2px; +} + +/* -------------------------------------------------------- + Generate Padding Classes (0px - 25px) + padding, padding-top, padding-bottom, padding-left, padding-right +-----------------------------------------------------------*/ +.padding (@label, @size: 1, @key:1) when (@size =< 30) { + .p-@{key} { + padding: @size !important; + } + + .p-t-@{key} { + padding-top: @size !important; + } + + .p-b-@{key} { + padding-bottom: @size !important; + } + + .p-l-@{key} { + padding-left: @size !important; + } + + .p-r-@{key} { + padding-right: @size !important; + } + + .padding(@label - 5; @size + 5; @key + 5); +} + +.padding(25, 0px, 0); + +/* -------------------------------------------------------- + Generate Font-Size Classes (8px - 20px) +-----------------------------------------------------------*/ +.font-size (@label, @size: 8, @key:10) when (@size =< 20) { + .f-@{key} { + font-size: @size !important; + } + + .font-size(@label - 1; @size + 1; @key + 1); +} + +.font-size(20, 8px, 8); + +.f-inherit { + font-size: inherit !important; +} + +/* -------------------------------------------------------- + Font Weight +-----------------------------------------------------------*/ +.f-300 { + font-weight: 300 !important; +} +.f-400 { + font-weight: 400 !important; +} +.f-500 { + font-weight: 500 !important; +} +.f-700 { + font-weight: 700 !important; +} + +/* -------------------------------------------------------- + Position +-----------------------------------------------------------*/ +.p-relative { + position: relative !important; +} +.p-absolute { + position: absolute !important; +} +.p-fixed { + position: fixed !important; +} +.p-static { + position: static !important; +} + +/* -------------------------------------------------------- + Overflow +-----------------------------------------------------------*/ +.o-hidden { + overflow: hidden !important; +} +.o-visible { + overflow: visible !important; +} +.o-auto { + overflow: auto !important; +} + +/* -------------------------------------------------------- + Display +-----------------------------------------------------------*/ +.di-block { + display: inline-block !important; +} +.d-block { + display: block; +} +.dashboard-parameters:has(.ParameterName-turnilo_daterange) { + display: none !important; +} +.dashboard-parameters:has(.parameter-1) { + display: inline-block !important; +} +.ParameterName-turnilo_daterange { + display: none !important; +} +/* -------------------------------------------------------- + Background Colors and Colors +-----------------------------------------------------------*/ +@array: + c-white bg-white @white, + c-ace bg-ace @ace, + c-black bg-black @black, + c-brown bg-brown @brown, + c-pink bg-pink @pink, + c-red bg-red @red, + c-blue bg-blue @blue, + c-purple bg-purple @purple, + c-deeppurple bg-deeppurple @deeppurple, + c-lightblue bg-lightblue @lightblue, + c-cyan bg-cyan @cyan, + c-teal bg-teal @teal, + c-green bg-green @green, + c-lightgreen bg-lightgreen @lightgreen, + c-lime bg-lime @lime, + c-yellow bg-yellow @yellow, + c-amber bg-amber @amber, + c-orange bg-orange @orange, + c-deeporange bg-deeporange @deeporange, + c-gray bg-gray @gray, + c-bluegray bg-bluegray @bluegray, + c-indigo bg-indigo @indigo; + +.for(@array); +.-each(@value) { + @name: extract(@value, 1); + @name2: extract(@value, 2); + @color: extract(@value, 3); + &.@{name2} { + background-color: @color !important; + } + + &.@{name} { + color: @color !important; + } +} + +/* -------------------------------------------------------- + Background Colors +-----------------------------------------------------------*/ +.bg-brand { + background-color: @brand-bg; +} +.bg-black-trp { + background-color: rgba(0, 0, 0, 0.12) !important; +} + +/* -------------------------------------------------------- + Borders +-----------------------------------------------------------*/ +.b-0 { + border: 0 !important; +} + +/* -------------------------------------------------------- + Width +-----------------------------------------------------------*/ +.w-100 { + width: 100% !important; +} +.w-50 { + width: 50% !important; +} +.w-25 { + width: 25% !important; +} + +/* -------------------------------------------------------- + Border Radius +-----------------------------------------------------------*/ +.brd-2 { + border-radius: 2px; +} + +/* -------------------------------------------------------- + Alignment +-----------------------------------------------------------*/ +.va-top { + vertical-align: top; +} diff --git a/client/app/assets/less/inc/header.less b/client/app/assets/less/inc/header.less index fea10d50c..7648bafe9 100755 --- a/client/app/assets/less/inc/header.less +++ b/client/app/assets/less/inc/header.less @@ -1,366 +1,367 @@ -#header { - width: 100%; - z-index: 10; - top: 0; - left: 0; - background-color: #fff; - height: @header-height; - - &.affix { - box-shadow: 0 0 20px rgba(0, 0, 0, 0.23); - } - - &:not(.affix) { - box-shadow: @tile-shadow; - position: fixed; - } -} - - -/* -------------------------------------------------------- - Top Menu ------------------------------------------------------------*/ -.header-inner { - padding: 0; - margin: 0; - width: 100%; - list-style: none; - - & > li { - &:not(.pull-right) { - float: left; - } - - @media (max-width: @screen-sm-min) { - &:not(.top-search) { - position: static; - } - - .dropdown-menu { - width: ~"calc(100% - 30px)"; - margin-left: 15px; - } - } - - & > a { - height: @header-height; - color: #333; - min-width: 45px; - display: block; - position: relative; - - & > .zmdi { - font-size: 22px; - line-height: @header-height; - } - } - - &:not(.logo) { - text-align: center; - } - - &.open > a:not([class*="hi-"]):before { - content: ""; - width: 40px; - height: 40px; - position: absolute; - top: 50%; - left: 50%; - margin-top: -21px; - margin-left: -20px; - background: #eee; - border-radius: 50%; - z-index: -1; - } - } - - .dropdown-menu { - margin-top: -5px; - } - - .open { - & > .hi-messages { color: @green; } - & > .hi-notifications { color: @orange; } - & > .hi-projects { color: @green; } - & > .hi-events { color: @blue; } - - .hi-count { - display: none; - } - } -} - -.hi-count { - position: absolute; - font-style: normal; - background-color: @red; - padding: 0 4px; - font-size: 10px; - color: #fff; - line-height: 17px; - height: 17px; - top: 11px; - right: 6px; - border-radius: 50%; - width: 17px; -} - -.hi-dropdown { - padding: 0; - - @media (min-width: @screen-sm-min) { - width: 350px; - } -} - -/* -------------------------------------------------------- - Logo ------------------------------------------------------------*/ -.logo { - position: relative; - z-index: 2; - height: @logo-height; - - @media (min-width: (@screen-lg-min + 80px)) { - width: @logo-width; - background-color: inherit; - margin-right: 15px; - - & > a { - padding: 15px 22px; - } - } - - @media (max-width: (@screen-md-max + 80px)) { - width: @sidebar-left-mid-width; - - & > a { - display: none !important; - } - } - - @media (max-width: (@screen-sm-min)) { - padding: 12px; - } -} - - -/* -------------------------------------------------------- - Sidebar Trigger for mobile ------------------------------------------------------------*/ -#menu-trigger { - font-size: 21px; - text-align: center; - color: #fff; - cursor: pointer; - display: none; - background: #000; - height: 100%; - - &.toggled i:before { - content: '\f2ea'; - } - - @media (min-width: (@screen-sm-min + 1)) { - line-height: @header-height; - } - - @media (max-width: (@screen-md-max + 80px)) { - display: block; - } - - @media (max-width: (@screen-sm-min)) { - border-radius: 2px; - line-height: 39px; - } -} - - -/* -------------------------------------------------------- - Top Search ------------------------------------------------------------*/ -.top-search { - position: relative; - background: #fff; - height: @header-height; - - &:not(.toggled) { - width: 80px; - margin-left: 15px; - - &:before { - font-family: @font-icon; - content: "\f1c3"; - position: absolute; - left: 0; - top: 15px; - font-size: 22px; - z-index: 1; - color: #333; - } - - .ts-reset { - display: none; - } - - .ts-input { - cursor: pointer; - } - - @media (max-width: (@screen-xs-min - 150px)) { - width: 20px; - } - } - - .ts-input { - height: @header-height - 2px; - padding-left: 25px; - width: 100%; - border: 0; - position: relative; - background: transparent; - z-index: 1; - } - - &.toggled { - position: absolute; - top: 0; - font-size: 20px; - font-weight: normal; - z-index: 1; - width: 100%; - left: 0; - - @media (min-width: (@screen-lg-min + 80px)) { - padding-left: @sidebar-left-width; - } - - @media (min-width: (@screen-sm-min + 1px)) and (max-width: (@screen-md-max + 80px)) { - padding-left: @sidebar-left-mid-width; - } - - .ts-input { - background: #fff; - } - - .ts-reset { - font-size: 11px; - color: #fff; - position: absolute; - top: 50%; - right: 15px; - z-index: 2; - width: 20px; - height: 20px; - background-color: #8E8E8E; - line-height: 20px; - text-align: center; - border-radius: 50%; - margin-top: -10px; - - &:hover { - cursor: pointer; - background: #333; - } - } - } -} - - -/* -------------------------------------------------------- - Events ------------------------------------------------------------*/ -.event-time { - width: 67px; - height: 50px; - text-align: center; - padding: 9px 0; - color: #fff; - border-radius: 2px; - margin-top: 2px; - - & > h2 { - margin: 0; - line-height: 100%; - font-size: 17px; - margin-bottom: -1px; - color: #fff; - font-weight: normal; - } -} - - -/* -------------------------------------------------------- - Apps ------------------------------------------------------------*/ -@media (min-width: @screen-sm-min) { - #launch-apps { - padding: 0; - text-align: center; - width: 300px; - } - - .la-body { - padding: 20px 10px; - } - - .lab-item { - width: 60px; - display: inline-block; - margin: 10px; - - &:hover { - & > a { - .opacity(0.8); - } - - & > small { - color: #333; - } - } - - & > a { - height: 60px; - display: block; - color: #fff; - line-height: 70px; - border-radius: 50%; - .transition(opacity); - - & > i { - font-size: 25px; - } - } - - & > small { - color: #969696; - display: block; - margin-top: 5px; - .transition(color); - } - - } -} - - -/* -------------------------------------------------------- - Time ------------------------------------------------------------*/ -#time { - font-size: 18px; - font-weight: 400; - background-color: @sidebar; - color: #FBFBFB; - padding: 4px 11px; - border-radius: 2px; - margin: 14px; - - span { - &:not(:last-child):after { - content: ":"; - position: relative; - top: -1px; - right: -1px; - } - } -} +#header { + width: 100%; + z-index: 10; + top: 0; + left: 0; + background-color: #fff; + height: @header-height; + + &.affix { + box-shadow: 0 0 20px rgba(0, 0, 0, 0.23); + } + + &:not(.affix) { + box-shadow: @tile-shadow; + position: fixed; + } +} + +/* -------------------------------------------------------- + Top Menu +-----------------------------------------------------------*/ +.header-inner { + padding: 0; + margin: 0; + width: 100%; + list-style: none; + + & > li { + &:not(.pull-right) { + float: left; + } + + @media (max-width: @screen-sm-min) { + &:not(.top-search) { + position: static; + } + + .dropdown-menu { + width: ~"calc(100% - 30px)"; + margin-left: 15px; + } + } + + & > a { + height: @header-height; + color: #333; + min-width: 45px; + display: block; + position: relative; + + & > .zmdi { + font-size: 22px; + line-height: @header-height; + } + } + + &:not(.logo) { + text-align: center; + } + + &.open > a:not([class*="hi-"]):before { + content: ""; + width: 40px; + height: 40px; + position: absolute; + top: 50%; + left: 50%; + margin-top: -21px; + margin-left: -20px; + background: #eee; + border-radius: 50%; + z-index: -1; + } + } + + .dropdown-menu { + margin-top: -5px; + } + + .open { + & > .hi-messages { + color: @green; + } + & > .hi-notifications { + color: @orange; + } + & > .hi-projects { + color: @green; + } + & > .hi-events { + color: @blue; + } + + .hi-count { + display: none; + } + } +} + +.hi-count { + position: absolute; + font-style: normal; + background-color: @red; + padding: 0 4px; + font-size: 10px; + color: #fff; + line-height: 17px; + height: 17px; + top: 11px; + right: 6px; + border-radius: 50%; + width: 17px; +} + +.hi-dropdown { + padding: 0; + + @media (min-width: @screen-sm-min) { + width: 350px; + } +} + +/* -------------------------------------------------------- + Logo +-----------------------------------------------------------*/ +.logo { + position: relative; + z-index: 2; + height: @logo-height; + + @media (min-width: (@screen-lg-min + 80px)) { + width: @logo-width; + background-color: inherit; + margin-right: 15px; + + & > a { + padding: 15px 22px; + } + } + + @media (max-width: (@screen-md-max + 80px)) { + width: @sidebar-left-mid-width; + + & > a { + display: none !important; + } + } + + @media (max-width: (@screen-sm-min)) { + padding: 12px; + } +} + +/* -------------------------------------------------------- + Sidebar Trigger for mobile +-----------------------------------------------------------*/ +#menu-trigger { + font-size: 21px; + text-align: center; + color: #fff; + cursor: pointer; + display: none; + background: #000; + height: 100%; + + &.toggled i:before { + content: "\f2ea"; + } + + @media (min-width: (@screen-sm-min + 1)) { + line-height: @header-height; + } + + @media (max-width: (@screen-md-max + 80px)) { + display: block; + } + + @media (max-width: (@screen-sm-min)) { + border-radius: 2px; + line-height: 39px; + } +} + +/* -------------------------------------------------------- + Top Search +-----------------------------------------------------------*/ +.top-search { + position: relative; + background: #fff; + height: @header-height; + + &:not(.toggled) { + width: 80px; + margin-left: 15px; + + &:before { + font-family: @font-icon; + content: "\f1c3"; + position: absolute; + left: 0; + top: 15px; + font-size: 22px; + z-index: 1; + color: #333; + } + + .ts-reset { + display: none; + } + + .ts-input { + cursor: pointer; + } + + @media (max-width: (@screen-xs-min - 150px)) { + width: 20px; + } + } + + .ts-input { + height: @header-height - 2px; + padding-left: 25px; + width: 100%; + border: 0; + position: relative; + background: transparent; + z-index: 1; + } + + &.toggled { + position: absolute; + top: 0; + font-size: 20px; + font-weight: normal; + z-index: 1; + width: 100%; + left: 0; + + @media (min-width: (@screen-lg-min + 80px)) { + padding-left: @sidebar-left-width; + } + + @media (min-width: (@screen-sm-min + 1px)) and (max-width: (@screen-md-max + 80px)) { + padding-left: @sidebar-left-mid-width; + } + + .ts-input { + background: #fff; + } + + .ts-reset { + font-size: 11px; + color: #fff; + position: absolute; + top: 50%; + right: 15px; + z-index: 2; + width: 20px; + height: 20px; + background-color: #8e8e8e; + line-height: 20px; + text-align: center; + border-radius: 50%; + margin-top: -10px; + + &:hover { + cursor: pointer; + background: #333; + } + } + } +} + +/* -------------------------------------------------------- + Events +-----------------------------------------------------------*/ +.event-time { + width: 67px; + height: 50px; + text-align: center; + padding: 9px 0; + color: #fff; + border-radius: 2px; + margin-top: 2px; + + & > h2 { + margin: 0; + line-height: 100%; + font-size: 17px; + margin-bottom: -1px; + color: #fff; + font-weight: normal; + } +} + +/* -------------------------------------------------------- + Apps +-----------------------------------------------------------*/ +@media (min-width: @screen-sm-min) { + #launch-apps { + padding: 0; + text-align: center; + width: 300px; + } + + .la-body { + padding: 20px 10px; + } + + .lab-item { + width: 60px; + display: inline-block; + margin: 10px; + + &:hover { + & > a { + .opacity(0.8); + } + + & > small { + color: #333; + } + } + + & > a { + height: 60px; + display: block; + color: #fff; + line-height: 70px; + border-radius: 50%; + .transition(opacity); + + & > i { + font-size: 25px; + } + } + + & > small { + color: #969696; + display: block; + margin-top: 5px; + .transition(color); + } + } +} + +/* -------------------------------------------------------- + Time +-----------------------------------------------------------*/ +#time { + font-size: 18px; + font-weight: 400; + background-color: @sidebar; + color: #fbfbfb; + padding: 4px 11px; + border-radius: 2px; + margin: 14px; + + span { + &:not(:last-child):after { + content: ":"; + position: relative; + top: -1px; + right: -1px; + } + } +} diff --git a/client/app/assets/less/inc/ie-warning.less b/client/app/assets/less/inc/ie-warning.less index 208f80ebb..1cdd1f3dc 100755 --- a/client/app/assets/less/inc/ie-warning.less +++ b/client/app/assets/less/inc/ie-warning.less @@ -1,53 +1,53 @@ .ie-warning { - position: fixed; - top: 0; - left: 0; - z-index: 9999; - background: @black; - width: 100%; - height: 100%; - text-align: center; - color: #fff; - font-family: "Courier New", Courier, monospace; - padding: 50px 0; + position: fixed; + top: 0; + left: 0; + z-index: 9999; + background: @black; + width: 100%; + height: 100%; + text-align: center; + color: #fff; + font-family: "Courier New", Courier, monospace; + padding: 50px 0; - p { - font-size: 17px; - } - - .iew-container { - min-width: 1024px; - width: 100%; - height: 200px; - background: #fff; - margin: 50px 0; - } + p { + font-size: 17px; + } + + .iew-container { + min-width: 1024px; + width: 100%; + height: 200px; + background: #fff; + margin: 50px 0; + } - .iew-download { - list-style: none; - padding: 30px 0; - margin: 0 auto; - width: 720px; + .iew-download { + list-style: none; + padding: 30px 0; + margin: 0 auto; + width: 720px; - & > li { - float: left; - vertical-align: top; + & > li { + float: left; + vertical-align: top; - & > a { - display: block; - color: #000; - width: 140px; - font-size: 15px; - padding: 15px 0; + & > a { + display: block; + color: #000; + width: 140px; + font-size: 15px; + padding: 15px 0; - & > div { - margin-top: 10px; - } + & > div { + margin-top: 10px; + } - &:hover { - background-color: #eee; - } - } - } - } -} \ No newline at end of file + &:hover { + background-color: #eee; + } + } + } + } +} diff --git a/client/app/assets/less/inc/jumbotron.less b/client/app/assets/less/inc/jumbotron.less index 38670381a..e473f60cd 100755 --- a/client/app/assets/less/inc/jumbotron.less +++ b/client/app/assets/less/inc/jumbotron.less @@ -1,4 +1,4 @@ -.jumbotron { - padding-left: 60px; - padding-right: 60px; -} \ No newline at end of file +.jumbotron { + padding-left: 60px; + padding-right: 60px; +} diff --git a/client/app/assets/less/inc/label.less b/client/app/assets/less/inc/label.less index dee027841..c7006eb14 100755 --- a/client/app/assets/less/inc/label.less +++ b/client/app/assets/less/inc/label.less @@ -1,37 +1,37 @@ -.label { - border-radius: 2px; - padding: 3px 6px 4px; - font-weight: 500; - font-size: 11px; -} - -.badge { - border-radius: 1px; -} - -.label-default { - background: fade(@redash-gray, 85%); -} - -.label-tag-unpublished { - background: fade(@redash-gray, 85%); -} - -.label-tag-archived { - .label-warning(); -} - -.label-tag { - background: fade(@redash-gray, 10%); - color: fade(@redash-gray, 75%); -} - -.label-tag-unpublished, -.label-tag-archived, -.label-tag { - margin-right: 3px; - display: inline; - margin-top: 2px; - max-width: 24ch; - .text-overflow(); -} \ No newline at end of file +.label { + border-radius: 2px; + padding: 3px 6px 4px; + font-weight: 500; + font-size: 11px; +} + +.badge { + border-radius: 1px; +} + +.label-default { + background: fade(@redash-gray, 85%); +} + +.label-tag-unpublished { + background: fade(@redash-gray, 85%); +} + +.label-tag-archived { + .label-warning(); +} + +.label-tag { + background: fade(@redash-gray, 10%); + color: fade(@redash-gray, 75%); +} + +.label-tag-unpublished, +.label-tag-archived, +.label-tag { + margin-right: 3px; + display: inline; + margin-top: 2px; + max-width: 24ch; + .text-overflow(); +} diff --git a/client/app/assets/less/inc/less-plugins/for.less b/client/app/assets/less/inc/less-plugins/for.less index e73830175..f5a5b5370 100755 --- a/client/app/assets/less/inc/less-plugins/for.less +++ b/client/app/assets/less/inc/less-plugins/for.less @@ -1,10 +1,19 @@ - -.for(@i, @n) {.-each(@i)} -.for(@n) when (isnumber(@n)) {.for(1, @n)} -.for(@i, @n) when not (@i = @n) { - .for((@i + (@n - @i) / abs(@n - @i)), @n); -} - -.for(@array) when (default()) {.for-impl_(length(@array))} -.for-impl_(@i) when (@i > 1) {.for-impl_((@i - 1))} -.for-impl_(@i) when (@i > 0) {.-each(extract(@array, @i))} +.for(@i, @n) { + .-each(@i); +} +.for(@n) when (isnumber(@n)) { + .for(1, @n); +} +.for(@i, @n) when not (@i = @n) { + .for((@i + (@n - @i) / abs(@n - @i)), @n); +} + +.for(@array) when (default()) { + .for-impl_(length(@array)); +} +.for-impl_(@i) when (@i > 1) { + .for-impl_((@i - 1)); +} +.for-impl_(@i) when (@i > 0) { + .-each(extract(@array, @i)); +} diff --git a/client/app/assets/less/inc/list-group.less b/client/app/assets/less/inc/list-group.less index f451c8be7..91a1fc8aa 100755 --- a/client/app/assets/less/inc/list-group.less +++ b/client/app/assets/less/inc/list-group.less @@ -1,20 +1,20 @@ .list-group { - margin-bottom: 0; + margin-bottom: 0; - &.lg-alt .list-group-item { - border: 0; - } + &.lg-alt .list-group-item { + border: 0; + } - &:not(.lg-alt) { - &.lg-listview .list-group-item { - border-left: 0; - border-right: 0; + &:not(.lg-alt) { + &.lg-listview .list-group-item { + border-left: 0; + border-right: 0; - &:last-child { - border-bottom: 0; - } - } + &:last-child { + border-bottom: 0; + } } + } } .max-character { @@ -22,50 +22,52 @@ } .list-group-item { - &.active { - button { - color: white; - } - } + &.active { + button { + color: white; + } + } .cr-alt { - line-height: 100%; - margin-top: 2px; + line-height: 100%; + margin-top: 2px; } - &.active, &.active:hover, &.active:focus { + &.active, + &.active:hover, + &.active:focus { background-color: #fff; box-shadow: inset 3px 0px 0px @brand-primary; } } .list-group-item-heading { - margin-bottom: 2px; - color: #333; + margin-bottom: 2px; + color: #333; - & > small { - font-size: 11px; - color: #C5C5C5; - margin-left: 10px; - } + & > small { + font-size: 11px; + color: #c5c5c5; + margin-left: 10px; + } } .list-group-item-heading, .list-group-item-text { - .text-overflow(); + .text-overflow(); } .list-group-item-text { - display: block; + display: block; - &:not(:last-child) { - margin-bottom: 4px; - } + &:not(:last-child) { + margin-bottom: 4px; + } } .list-group-img { - width: 38px; - height: 38px; - border-radius: 2px; + width: 38px; + height: 38px; + border-radius: 2px; } .ui-select-choices-row.disabled > span { @@ -81,4 +83,4 @@ color: #333; pointer-events: none; cursor: not-allowed; -} \ No newline at end of file +} diff --git a/client/app/assets/less/inc/list.less b/client/app/assets/less/inc/list.less index 611032103..4b2e16578 100755 --- a/client/app/assets/less/inc/list.less +++ b/client/app/assets/less/inc/list.less @@ -1,23 +1,23 @@ -.clist { - list-style: none; - - & > li { - &:before { - font-family: @font-icon; - margin: 0 10px 0 -20px; - vertical-align: middle; - } - } - - &.clist-angle > li:before { - content: "\f2fb"; - } - - &.clist-check > li:before { - content: "\f26b"; - } - - &.clist-star > li:before { - content: "\f27d"; - } -} \ No newline at end of file +.clist { + list-style: none; + + & > li { + &:before { + font-family: @font-icon; + margin: 0 10px 0 -20px; + vertical-align: middle; + } + } + + &.clist-angle > li:before { + content: "\f2fb"; + } + + &.clist-check > li:before { + content: "\f26b"; + } + + &.clist-star > li:before { + content: "\f27d"; + } +} diff --git a/client/app/assets/less/inc/login.less b/client/app/assets/less/inc/login.less index 81ee07779..f1931953b 100755 --- a/client/app/assets/less/inc/login.less +++ b/client/app/assets/less/inc/login.less @@ -1,138 +1,136 @@ -.login-content { - overflow: hidden; - height: 100%; - background: @brand-bg; - padding: 0; - text-align: center; - - &:after { - content: ""; - vertical-align: middle; - display: inline-block; - width: 1px; - height: 100vh; - } -} - -.lc-block { - background: #fff; - box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); - border-radius: 2px; - width: 300px; - display: inline-block; - vertical-align: middle; - position: relative; - padding: 45px 30px 30px; - - &:not(.toggled) { - display: none; - } - - &.toggled { - .animated(fadeInUp, 300ms); - z-index: 10; - } - - @media (max-width: @screen-xs-max) { - padding: 15px 35px 25px 20px; - width: ~"calc(100% - 60px)"; - } - - .form-control { - text-align: center; - } -} - -.lcb-float { - width: 60px; - height: 60px; - background: #ffffff; - border-radius: 50%; - box-shadow: 0 -10px 19px rgba(0, 0, 0, 0.38); - position: absolute; - top: -35px; - left: 50%; - margin-left: -30px; - - img { - width: 100%; - height: 100%; - border-radius: 50%; - padding: 4px; - } - - i { - color: #333; - font-size: 25px; - line-height: 60px; - } -} - -.lcb-lockscreen { - position: relative; - - .form-control { - padding-right: 35px; - } - - .lcbl-btn { - background-color: #2196F3; - position: absolute; - top: 0; - right: 0; - width: 30px; - color: #fff; - font-size: 15px; - height: 27px; - margin: 4px; - line-height: 26px; - border-radius: 2px; - } -} - -.login-navigation { - list-style: none; - padding: 0; - margin: 0; - position: absolute; - width: 100%; - text-align: center; - left: 0%; - bottom: -45px; - - & > li { - display: inline-block; - margin: 0 2px; - .transition(all); - .transition-duration(150ms); - cursor: pointer; - vertical-align: top; - color: #fff; - line-height: 16px; - min-width: 16px; - min-height: 16px; - text-transform: uppercase; - .backface-visibility(hidden); - - & > span { - .opacity(0); - } - - &:not(:hover) { - font-size: 0px; - border-radius: 100%; - } - - &:hover { - border-radius: 10px; - padding: 0 5px; - font-size: 8px; - - & > span { - .opacity(1); - } - } - - } -} - \ No newline at end of file +.login-content { + overflow: hidden; + height: 100%; + background: @brand-bg; + padding: 0; + text-align: center; + + &:after { + content: ""; + vertical-align: middle; + display: inline-block; + width: 1px; + height: 100vh; + } +} + +.lc-block { + background: #fff; + box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); + border-radius: 2px; + width: 300px; + display: inline-block; + vertical-align: middle; + position: relative; + padding: 45px 30px 30px; + + &:not(.toggled) { + display: none; + } + + &.toggled { + .animated(fadeInUp, 300ms); + z-index: 10; + } + + @media (max-width: @screen-xs-max) { + padding: 15px 35px 25px 20px; + width: ~"calc(100% - 60px)"; + } + + .form-control { + text-align: center; + } +} + +.lcb-float { + width: 60px; + height: 60px; + background: #ffffff; + border-radius: 50%; + box-shadow: 0 -10px 19px rgba(0, 0, 0, 0.38); + position: absolute; + top: -35px; + left: 50%; + margin-left: -30px; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + padding: 4px; + } + + i { + color: #333; + font-size: 25px; + line-height: 60px; + } +} + +.lcb-lockscreen { + position: relative; + + .form-control { + padding-right: 35px; + } + + .lcbl-btn { + background-color: #2196f3; + position: absolute; + top: 0; + right: 0; + width: 30px; + color: #fff; + font-size: 15px; + height: 27px; + margin: 4px; + line-height: 26px; + border-radius: 2px; + } +} + +.login-navigation { + list-style: none; + padding: 0; + margin: 0; + position: absolute; + width: 100%; + text-align: center; + left: 0%; + bottom: -45px; + + & > li { + display: inline-block; + margin: 0 2px; + .transition(all); + .transition-duration(150ms); + cursor: pointer; + vertical-align: top; + color: #fff; + line-height: 16px; + min-width: 16px; + min-height: 16px; + text-transform: uppercase; + .backface-visibility(hidden); + + & > span { + .opacity(0); + } + + &:not(:hover) { + font-size: 0px; + border-radius: 100%; + } + + &:hover { + border-radius: 10px; + padding: 0 5px; + font-size: 8px; + + & > span { + .opacity(1); + } + } + } +} diff --git a/client/app/assets/less/inc/media.less b/client/app/assets/less/inc/media.less index 59879f262..db05917c1 100755 --- a/client/app/assets/less/inc/media.less +++ b/client/app/assets/less/inc/media.less @@ -1,124 +1,123 @@ -/* -------------------------------------------------------- - Thumbnail ------------------------------------------------------------*/ -.thumbnail { - a&:hover, - a&:focus, - a&.active { - border-color: @thumbnail-border; - } -} - - -/* -------------------------------------------------------- - Lightbox ------------------------------------------------------------*/ -.lightbox { - & > a { - position: relative; - .transition(opacity); - .transition-duration(300ms); - - & > img { - width: 100%; - } - - &:hover { - .opacity(0.8); - } - } - - & > a:not(.p-item) { //Not for photo items - margin-bottom: 20px; - } -} - - -/* -------------------------------------------------------- - Carousel ------------------------------------------------------------*/ -.carousel { - .carousel-control { - .transition(all); - .transition-duration(250ms); - .opacity(0); - - .zmdi { - position: absolute; - top: 50%; - left: 50%; - line-height: 100%; - - @media screen and (min-width: @screen-sm-min) { - font-size: 60px; - width: 60px; - height: 60px; - margin-top: -30px; - margin-left: -30px; - } - - @media screen and (max-width: @screen-sm-max) { - width: 24px; - height: 24px; - margin-top: -12px; - margin-left: -12px; - } - } - } - - &:hover { - .carousel-control { - .opacity(1); - } - } - - .carousel-caption { - background: rgba(0,0,0,0.6); - left: 0; - right: 0; - bottom: 0; - width: 100%; - padding-bottom: 50px; - - & > h3 { - color: #fff; - margin: 0 0 5px; - font-weight: 300; - } - - & > p { - margin: 0; - } - - @media screen and (max-width: @screen-sm-max) { - display: none; - } - } - - .carousel-indicators { - bottom: 10px; - margin: 0; - left: 0; - bottom: 0; - width: 100%; - padding: 0 0 6px; - background: rgba(0,0,0,0.6); - - li { - border-radius: 0; - width: 15px; - border: 0; - background: #fff; - height: 3px; - margin: 0; - .transition(all); - .transition-duration(250ms); - - &.active { - width: 25px; - height: 3px; - background: @orange; - } - } - } -} \ No newline at end of file +/* -------------------------------------------------------- + Thumbnail +-----------------------------------------------------------*/ +.thumbnail { + a&:hover, + a&:focus, + a&.active { + border-color: @thumbnail-border; + } +} + +/* -------------------------------------------------------- + Lightbox +-----------------------------------------------------------*/ +.lightbox { + & > a { + position: relative; + .transition(opacity); + .transition-duration(300ms); + + & > img { + width: 100%; + } + + &:hover { + .opacity(0.8); + } + } + + & > a:not(.p-item) { + //Not for photo items + margin-bottom: 20px; + } +} + +/* -------------------------------------------------------- + Carousel +-----------------------------------------------------------*/ +.carousel { + .carousel-control { + .transition(all); + .transition-duration(250ms); + .opacity(0); + + .zmdi { + position: absolute; + top: 50%; + left: 50%; + line-height: 100%; + + @media screen and (min-width: @screen-sm-min) { + font-size: 60px; + width: 60px; + height: 60px; + margin-top: -30px; + margin-left: -30px; + } + + @media screen and (max-width: @screen-sm-max) { + width: 24px; + height: 24px; + margin-top: -12px; + margin-left: -12px; + } + } + } + + &:hover { + .carousel-control { + .opacity(1); + } + } + + .carousel-caption { + background: rgba(0, 0, 0, 0.6); + left: 0; + right: 0; + bottom: 0; + width: 100%; + padding-bottom: 50px; + + & > h3 { + color: #fff; + margin: 0 0 5px; + font-weight: 300; + } + + & > p { + margin: 0; + } + + @media screen and (max-width: @screen-sm-max) { + display: none; + } + } + + .carousel-indicators { + bottom: 10px; + margin: 0; + left: 0; + bottom: 0; + width: 100%; + padding: 0 0 6px; + background: rgba(0, 0, 0, 0.6); + + li { + border-radius: 0; + width: 15px; + border: 0; + background: #fff; + height: 3px; + margin: 0; + .transition(all); + .transition-duration(250ms); + + &.active { + width: 25px; + height: 3px; + background: @orange; + } + } + } +} diff --git a/client/app/assets/less/inc/messages.less b/client/app/assets/less/inc/messages.less index 24fe0e5a8..47b2a7f03 100755 --- a/client/app/assets/less/inc/messages.less +++ b/client/app/assets/less/inc/messages.less @@ -1,161 +1,160 @@ -#messages-main { - position: relative; - margin: 0 auto; - .clearfix(); - - .ms-menu { - position: absolute; - left: 0; - top: 0; - border-right: 1px solid #eee; - padding-bottom: 50px; - height: 100%; - width: 240px; - background: #fff; - - @media (max-width: @screen-xs-max) { - height: ~"calc(100% - 58px)"; - display: none; - z-index: 1; - top: 58px; - - &.toggled { - display: block; - } - } - } - - .ms-body { - @media (min-width: @screen-sm-min) { - padding-left: 240px; - } - - @media (max-width: @screen-xs-max) { - overflow: hidden; - } - } - - .ms-user { - padding: 15px; - background: @ace; - - & > div { - overflow: hidden; - padding: 3px 5px 0px 15px; - font-size: 11px; - } - } - - #ms-compose { - position: fixed; - bottom: 120px; - z-index: 1; - right: 30px; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.14),0 4px 8px rgba(0, 0, 0, 0.28); - } -} - -#ms-menu-trigger { - .user-select(none); - position: absolute; - left: 0; - top: 0; - width: 50px; - height: 100%; - text-align: right; - padding-right: 10px; - padding-top: 19px; - - i { - font-size: 21px; - } - - &.toggled { - i:before{ - content: '\f2ea'; - } - } -} - - -/* -------------------------------------------------------- - For Message ------------------------------------------------------------*/ -.message-feed { - padding: 20px; - - &.right { - text-align: right; - - & > .pull-right { - margin-left: 15px; - } - } - - &:not(.right) { - .mf-content { - background: @amber; - color: #fff; - } - - - } - - &.right .mf-content { - background: #eee; - } -} - -.mf-content { - padding: 12px 17px 13px; - border-radius: 2px; - display: inline-block; - max-width: 80%; -} - -.mf-date { - display: block; - color: #B3B3B3; - margin-top: 7px; - - & > i { - font-size: 14px; - line-height: 100%; - position: relative; - top: 1px; - } -} - -.msb-reply { - box-shadow: 0 -20px 20px -5px #fff; - position: relative; - margin-top: 30px; - border-top: 1px solid #eee; - background: @ace; - - textarea { - width: 100%; - font-size: 13px; - border: 0; - padding: 10px 15px; - resize: none; - height: 60px; - background: transparent; - } - - button { - position: absolute; - top: 0; - right: 0; - border: 0; - height: 100%; - width: 60px; - font-size: 25px; - color: @blue; - background: transparent; - - &:hover { - background: #f2f2f2; - } - } -} \ No newline at end of file +#messages-main { + position: relative; + margin: 0 auto; + .clearfix(); + + .ms-menu { + position: absolute; + left: 0; + top: 0; + border-right: 1px solid #eee; + padding-bottom: 50px; + height: 100%; + width: 240px; + background: #fff; + + @media (max-width: @screen-xs-max) { + height: ~"calc(100% - 58px)"; + display: none; + z-index: 1; + top: 58px; + + &.toggled { + display: block; + } + } + } + + .ms-body { + @media (min-width: @screen-sm-min) { + padding-left: 240px; + } + + @media (max-width: @screen-xs-max) { + overflow: hidden; + } + } + + .ms-user { + padding: 15px; + background: @ace; + + & > div { + overflow: hidden; + padding: 3px 5px 0px 15px; + font-size: 11px; + } + } + + #ms-compose { + position: fixed; + bottom: 120px; + z-index: 1; + right: 30px; + box-shadow: + 0 0 4px rgba(0, 0, 0, 0.14), + 0 4px 8px rgba(0, 0, 0, 0.28); + } +} + +#ms-menu-trigger { + .user-select(none); + position: absolute; + left: 0; + top: 0; + width: 50px; + height: 100%; + text-align: right; + padding-right: 10px; + padding-top: 19px; + + i { + font-size: 21px; + } + + &.toggled { + i:before { + content: "\f2ea"; + } + } +} + +/* -------------------------------------------------------- + For Message +-----------------------------------------------------------*/ +.message-feed { + padding: 20px; + + &.right { + text-align: right; + + & > .pull-right { + margin-left: 15px; + } + } + + &:not(.right) { + .mf-content { + background: @amber; + color: #fff; + } + } + + &.right .mf-content { + background: #eee; + } +} + +.mf-content { + padding: 12px 17px 13px; + border-radius: 2px; + display: inline-block; + max-width: 80%; +} + +.mf-date { + display: block; + color: #b3b3b3; + margin-top: 7px; + + & > i { + font-size: 14px; + line-height: 100%; + position: relative; + top: 1px; + } +} + +.msb-reply { + box-shadow: 0 -20px 20px -5px #fff; + position: relative; + margin-top: 30px; + border-top: 1px solid #eee; + background: @ace; + + textarea { + width: 100%; + font-size: 13px; + border: 0; + padding: 10px 15px; + resize: none; + height: 60px; + background: transparent; + } + + button { + position: absolute; + top: 0; + right: 0; + border: 0; + height: 100%; + width: 60px; + font-size: 25px; + color: @blue; + background: transparent; + + &:hover { + background: #f2f2f2; + } + } +} diff --git a/client/app/assets/less/inc/misc.less b/client/app/assets/less/inc/misc.less index a4bfabf6d..386e9e3eb 100755 --- a/client/app/assets/less/inc/misc.less +++ b/client/app/assets/less/inc/misc.less @@ -1,242 +1,236 @@ -/* -------------------------------------------------------- - Actions ------------------------------------------------------------*/ -.actions { - position: absolute; - list-style: none; - padding: 0; - margin: 0; - - & > li { - display: inline-block; - - & > a { - display: block; - padding: 0 10px; - - & > i { - font-size: 20px; - } - } - } - - .dropdown-menu { - min-width: 140px; - margin-top: -8px; - margin-right: -1px; - } - - &:not(.a-alt) { - & > li > a > i { - color: #939393; - } - - & > li.open > a > i, - & > li > a:hover > i { - color: #000; - } - } - - &.a-alt { - & > li > a > i { - color: #fff; - } - } -} - - -/* -------------------------------------------------------- - View More ------------------------------------------------------------*/ -.view-more { - display: block; - padding: 5px 10px; - text-align: center; - border-top: 1px solid darken(@light-gray, 3%); - font-size: 12px; - margin-top: 15px; - color: #777777; - - &:hover { - color: #333; - background-color: @light-gray; - } -} - - -/* -------------------------------------------------------- - Page Header ------------------------------------------------------------*/ -.page-header { - padding: 0 22px; - font-weight: normal; - font-size: 19px; - margin: 0 0 20px 0; - - small { - text-transform: none; - display: block; - font-size: 12px; - color: #9C9C9C; - margin-top: 7px; - line-height: 140%; - } - - h3 { - margin: 0; - font-weight: normal; - font-size: 15px; - color: #333; - } -} - - -/* -------------------------------------------------------- - Close ------------------------------------------------------------*/ -.close { - font-weight: normal; - text-shadow: none; - .opacity(0.5); -} - - -/* -------------------------------------------------------- - Action Header ------------------------------------------------------------*/ -.action-header { - position: relative; - background: @ace; - padding: 15px 13px 15px 17px; -} - -.ah-actions { - z-index: 3; - float: right; - margin-top: 7px; - position: relative; -} - -.ah-label { - color: #818181; - display: inline-block; - margin: 0; - font-size: 14px; - font-weight: normal; - padding: 0 6px; - line-height: 33px; - vertical-align: middle; - float: left; -} - -.ah-search { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 4; - background: #fff; - display: none; - - &:before { - content: "\f1c3"; - font-family: 'Material-Design-Iconic-Font'; - position: absolute; - left: 24px; - top: 17px; - font-size: 22px; - } -} - -.ahs-input { - border: 0; - padding: 0 26px 0 55px; - height: 63px; - font-size: 18px; - width: 100%; - font-weight: 100; - background: #fff; - border-bottom: 1px solid #EEE; -} - -.ahs-close { - font-style: normal; - position: absolute; - top: 23px; - right: 22px; - font-size: 17px; - width: 18px; - height: 18px; - background-color: #ADADAD; - line-height: 100%; - color: #fff; - text-align: center; - cursor: pointer; - border-radius: 50%; - - &:hover { - background: #333; - } -} - - -/* -------------------------------------------------------- - Load More ------------------------------------------------------------*/ -.load-more { - text-align: center; - margin-top: 30px; - - a { - padding: 5px 10px 3px; - display: inline-block; - background-color: @red; - color: #FFF; - border-radius: 2px; - white-space: nowrap; - - i { - font-size: 20px; - vertical-align: middle; - position: relative; - margin-top: -2px; - } - - &:hover { - background-color: darken(@red, 10%); - } - } -} - - -/* -------------------------------------------------------- - Data List ------------------------------------------------------------*/ -.dl-horizontal dt { - text-align: left; -} - - -/* -------------------------------------------------------- - User Avatar ------------------------------------------------------------*/ -.img-avatar { - height: 37px; - border-radius: 2px; - width: 37px; -} - -/* -------------------------------------------------------- - Percy ------------------------------------------------------------*/ -@media only percy { - .hide-in-percy, .pace { - visibility: hidden; - } - - // hide tooltips in Percy - .ant-tooltip { - display: none !important; - } -} \ No newline at end of file +/* -------------------------------------------------------- + Actions +-----------------------------------------------------------*/ +.actions { + position: absolute; + list-style: none; + padding: 0; + margin: 0; + + & > li { + display: inline-block; + + & > a { + display: block; + padding: 0 10px; + + & > i { + font-size: 20px; + } + } + } + + .dropdown-menu { + min-width: 140px; + margin-top: -8px; + margin-right: -1px; + } + + &:not(.a-alt) { + & > li > a > i { + color: #939393; + } + + & > li.open > a > i, + & > li > a:hover > i { + color: #000; + } + } + + &.a-alt { + & > li > a > i { + color: #fff; + } + } +} + +/* -------------------------------------------------------- + View More +-----------------------------------------------------------*/ +.view-more { + display: block; + padding: 5px 10px; + text-align: center; + border-top: 1px solid darken(@light-gray, 3%); + font-size: 12px; + margin-top: 15px; + color: #777777; + + &:hover { + color: #333; + background-color: @light-gray; + } +} + +/* -------------------------------------------------------- + Page Header +-----------------------------------------------------------*/ +.page-header { + padding: 0 22px; + font-weight: normal; + font-size: 19px; + margin: 0 0 20px 0; + + small { + text-transform: none; + display: block; + font-size: 12px; + color: #9c9c9c; + margin-top: 7px; + line-height: 140%; + } + + h3 { + margin: 0; + font-weight: normal; + font-size: 15px; + color: #333; + } +} + +/* -------------------------------------------------------- + Close +-----------------------------------------------------------*/ +.close { + font-weight: normal; + text-shadow: none; + .opacity(0.5); +} + +/* -------------------------------------------------------- + Action Header +-----------------------------------------------------------*/ +.action-header { + position: relative; + background: @ace; + padding: 15px 13px 15px 17px; +} + +.ah-actions { + z-index: 3; + float: right; + margin-top: 7px; + position: relative; +} + +.ah-label { + color: #818181; + display: inline-block; + margin: 0; + font-size: 14px; + font-weight: normal; + padding: 0 6px; + line-height: 33px; + vertical-align: middle; + float: left; +} + +.ah-search { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 4; + background: #fff; + display: none; + + &:before { + content: "\f1c3"; + font-family: "Material-Design-Iconic-Font"; + position: absolute; + left: 24px; + top: 17px; + font-size: 22px; + } +} + +.ahs-input { + border: 0; + padding: 0 26px 0 55px; + height: 63px; + font-size: 18px; + width: 100%; + font-weight: 100; + background: #fff; + border-bottom: 1px solid #eee; +} + +.ahs-close { + font-style: normal; + position: absolute; + top: 23px; + right: 22px; + font-size: 17px; + width: 18px; + height: 18px; + background-color: #adadad; + line-height: 100%; + color: #fff; + text-align: center; + cursor: pointer; + border-radius: 50%; + + &:hover { + background: #333; + } +} + +/* -------------------------------------------------------- + Load More +-----------------------------------------------------------*/ +.load-more { + text-align: center; + margin-top: 30px; + + a { + padding: 5px 10px 3px; + display: inline-block; + background-color: @red; + color: #fff; + border-radius: 2px; + white-space: nowrap; + + i { + font-size: 20px; + vertical-align: middle; + position: relative; + margin-top: -2px; + } + + &:hover { + background-color: darken(@red, 10%); + } + } +} + +/* -------------------------------------------------------- + Data List +-----------------------------------------------------------*/ +.dl-horizontal dt { + text-align: left; +} + +/* -------------------------------------------------------- + User Avatar +-----------------------------------------------------------*/ +.img-avatar { + height: 37px; + border-radius: 2px; + width: 37px; +} + +/* -------------------------------------------------------- + Percy +-----------------------------------------------------------*/ +@media only percy { + .hide-in-percy, + .pace { + visibility: hidden; + } + + // hide tooltips in Percy + .ant-tooltip { + display: none !important; + } +} diff --git a/client/app/assets/less/inc/mixins.less b/client/app/assets/less/inc/mixins.less index 425d38fa3..e9ce8c6f8 100755 --- a/client/app/assets/less/inc/mixins.less +++ b/client/app/assets/less/inc/mixins.less @@ -1,82 +1,81 @@ -/* -------------------------------------------------------- - Font Face ------------------------------------------------------------*/ -.font-face(@family, @name, @weight: 300, @style){ - @font-face{ - font-family: @family; - src:url('../fonts/@{family}/@{name}.eot'); - src:url('../fonts/@{family}/@{name}.eot?#iefix') format('embedded-opentype'), - url('../fonts/@{family}/@{name}.woff') format('woff'), - url('../fonts/@{family}/@{name}.ttf') format('truetype'), - url('../fonts/@{family}/@{name}.svg#icon') format('svg'); - font-weight: @weight; - font-style: @style; - } -} - -/* -------------------------------------------------------- - Button Varients ------------------------------------------------------------*/ -.button-variant(@color; @background; @border) { - color: @color; - background-color: @background; - border-color: @border; - - &:hover, - &:focus, - &.focus, - &:active, - &.active, - .open > .dropdown-toggle& { - color: @color; - background-color: darken(@background, 2%); - border-color: darken(@border, 1%); - } - &:active, - &.active, - .open > .dropdown-toggle& { - background-image: none; - } - &.disabled, - &[disabled], - fieldset[disabled] & { - &, - &:hover, - &:focus, - &.focus, - &:active, - &.active { - background-color: @background; - border-color: @border; - } - } - - .badge { - color: @background; - background-color: @color; - } -} - - -/* -------------------------------------------------------- - CSS Transform - Scale and Rotate ------------------------------------------------------------*/ -.scale-rotate(@scale, @rotate) { - -webkit-transform: scale(@scale) rotate(@rotate); - -ms-transform: scale(@scale) rotate(@rotate); - -o-transform: scale(@scale) rotate(@rotate); - transform: scale(@scale) rotate(@rotate); -} - - -/* -------------------------------------------------------- - CSS Animations based on animate.css ------------------------------------------------------------*/ -.animated(@name, @duration) { - -webkit-animation-name: @name; - animation-name: @name; - -webkit-animation-duration: @duration; - animation-duration: @duration; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} +/* -------------------------------------------------------- + Font Face +-----------------------------------------------------------*/ +.font-face(@family, @name, @weight: 300, @style) { + @font-face { + font-family: @family; + src: url("../fonts/@{family}/@{name}.eot"); + src: + url("../fonts/@{family}/@{name}.eot?#iefix") format("embedded-opentype"), + url("../fonts/@{family}/@{name}.woff") format("woff"), + url("../fonts/@{family}/@{name}.ttf") format("truetype"), + url("../fonts/@{family}/@{name}.svg#icon") format("svg"); + font-weight: @weight; + font-style: @style; + } +} + +/* -------------------------------------------------------- + Button Varients +-----------------------------------------------------------*/ +.button-variant(@color; @background; @border) { + color: @color; + background-color: @background; + border-color: @border; + + &:hover, + &:focus, + &.focus, + &:active, + &.active, + .open > .dropdown-toggle& { + color: @color; + background-color: darken(@background, 2%); + border-color: darken(@border, 1%); + } + &:active, + &.active, + .open > .dropdown-toggle& { + background-image: none; + } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &.focus, + &:active, + &.active { + background-color: @background; + border-color: @border; + } + } + + .badge { + color: @background; + background-color: @color; + } +} + +/* -------------------------------------------------------- + CSS Transform - Scale and Rotate +-----------------------------------------------------------*/ +.scale-rotate(@scale, @rotate) { + -webkit-transform: scale(@scale) rotate(@rotate); + -ms-transform: scale(@scale) rotate(@rotate); + -o-transform: scale(@scale) rotate(@rotate); + transform: scale(@scale) rotate(@rotate); +} + +/* -------------------------------------------------------- + CSS Animations based on animate.css +-----------------------------------------------------------*/ +.animated(@name, @duration) { + -webkit-animation-name: @name; + animation-name: @name; + -webkit-animation-duration: @duration; + animation-duration: @duration; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} diff --git a/client/app/assets/less/inc/modal.less b/client/app/assets/less/inc/modal.less index 0cba80ecf..e51cbe471 100755 --- a/client/app/assets/less/inc/modal.less +++ b/client/app/assets/less/inc/modal.less @@ -1,17 +1,17 @@ .modal-header { - padding: 23px 26px; + padding: 23px 26px; } .modal-body { - padding: 0 26px 10px; + padding: 0 26px 10px; } .modal-content { - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.31); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.31); } .modal-footer { - padding: 20px 26px; + padding: 20px 26px; } .modal-xl { diff --git a/client/app/assets/less/inc/panel.less b/client/app/assets/less/inc/panel.less index 40d1b3033..c932558ab 100755 --- a/client/app/assets/less/inc/panel.less +++ b/client/app/assets/less/inc/panel.less @@ -1,106 +1,106 @@ -.panel { - box-shadow: none; - border: 0; -} - -.panel-heading { - padding: 0; - >p { - &:last-child { - margin-bottom: 0px; - } - } - >a, .query-link { - color: inherit; - } - .query-link { - &:hover { - text-decoration: underline; - } - } -} - -.panel-title { - & > a { - padding: 10px 15px; - display: block; - font-size: 13px; - } -} - -.panel-collapse { - .panel-heading { - position: relative; - - .panel-title { - & > a { - padding: 8px 5px 16px 30px; - color: #000; - position: relative; - border-bottom: 2px solid #eee; - } - } - - &:before { - font-family: @font-icon; - font-size: 17px; - position: absolute; - left: 0; - top: 4px; - content: "\f278"; - } - - &.active { - &:before { - content: "\f273"; - } - } - } - - - .panel-body { - border-top: 0 !important; - padding-left: 5px; - padding-right: 5px; - } -} - -.panel-collapse-color(@color) { - .panel-collapse { - .panel-heading { - &.active .panel-title > a { - border-bottom-color: @color; - } - } - } -} - -.panel-group { - &:not([data-collapse-color]) { - .panel-collapse-color(@blue); - } - - &[data-collapse-color="red"] { - .panel-collapse-color(@red); - } - - &[data-collapse-color="green"] { - .panel-collapse-color(@green); - } - - &[data-collapse-color="amber"] { - .panel-collapse-color(@amber); - } - - &[data-collapse-color="teal"] { - .panel-collapse-color(@teal); - } - - &[data-collapse-color="black"] { - .panel-collapse-color(@black); - } - - &[data-collapse-color="cyan"] { - .panel-collapse-color(@cyan); - } -} +.panel { + box-shadow: none; + border: 0; +} + +.panel-heading { + padding: 0; + > p { + &:last-child { + margin-bottom: 0px; + } + } + > a, + .query-link { + color: inherit; + } + .query-link { + &:hover { + text-decoration: underline; + } + } +} + +.panel-title { + & > a { + padding: 10px 15px; + display: block; + font-size: 13px; + } +} + +.panel-collapse { + .panel-heading { + position: relative; + + .panel-title { + & > a { + padding: 8px 5px 16px 30px; + color: #000; + position: relative; + border-bottom: 2px solid #eee; + } + } + + &:before { + font-family: @font-icon; + font-size: 17px; + position: absolute; + left: 0; + top: 4px; + content: "\f278"; + } + + &.active { + &:before { + content: "\f273"; + } + } + } + + .panel-body { + border-top: 0 !important; + padding-left: 5px; + padding-right: 5px; + } +} + +.panel-collapse-color(@color) { + .panel-collapse { + .panel-heading { + &.active .panel-title > a { + border-bottom-color: @color; + } + } + } +} + +.panel-group { + &:not([data-collapse-color]) { + .panel-collapse-color(@blue); + } + + &[data-collapse-color="red"] { + .panel-collapse-color(@red); + } + + &[data-collapse-color="green"] { + .panel-collapse-color(@green); + } + + &[data-collapse-color="amber"] { + .panel-collapse-color(@amber); + } + + &[data-collapse-color="teal"] { + .panel-collapse-color(@teal); + } + + &[data-collapse-color="black"] { + .panel-collapse-color(@black); + } + + &[data-collapse-color="cyan"] { + .panel-collapse-color(@cyan); + } +} diff --git a/client/app/assets/less/inc/photos.less b/client/app/assets/less/inc/photos.less index 0f4e4b8a1..ea5d8b007 100755 --- a/client/app/assets/less/inc/photos.less +++ b/client/app/assets/less/inc/photos.less @@ -1,71 +1,70 @@ -.photos { - &:not(.pmb-block) { - margin: 10px 5px 0; - } - - .p-item { - padding: 0 3px; - margin-bottom: 6px; - } -} - -.p-item { - & > img { - border-radius: 2px; - } -} - -.p-timeline { - position: relative; - padding-left: 80px; - margin-bottom: 75px; - - .p-item { - float: left; - width: 70px; - height: 70px; - margin: 0 3px 3px 0; - } - - - &:last-child .pt-line:before { - height: 100%; - } -} - -.ptb-title { - font-size: 15px; - font-weight: 400; - margin-bottom: 20px; -} - -.pt-line { - position: absolute; - left: 0; - top: 0; - height: 100%; - line-height: 14px; - - &:before, - &:after { - content: ""; - position: absolute; - } - - &:before { - width: 1px; - height: ~"calc(100% + 63px)"; - background: #E2E2E2; - top: 14px; - right: -20px; - } - - &:after { - top: 2px; - right: -26px; - width: 13px; - height: 13px; - border: 1px solid #C1C1C1; - border-radius: 50%; - } -} \ No newline at end of file +.photos { + &:not(.pmb-block) { + margin: 10px 5px 0; + } + + .p-item { + padding: 0 3px; + margin-bottom: 6px; + } +} + +.p-item { + & > img { + border-radius: 2px; + } +} + +.p-timeline { + position: relative; + padding-left: 80px; + margin-bottom: 75px; + + .p-item { + float: left; + width: 70px; + height: 70px; + margin: 0 3px 3px 0; + } + + &:last-child .pt-line:before { + height: 100%; + } +} + +.ptb-title { + font-size: 15px; + font-weight: 400; + margin-bottom: 20px; +} + +.pt-line { + position: absolute; + left: 0; + top: 0; + height: 100%; + line-height: 14px; + + &:before, + &:after { + content: ""; + position: absolute; + } + + &:before { + width: 1px; + height: ~"calc(100% + 63px)"; + background: #e2e2e2; + top: 14px; + right: -20px; + } + + &:after { + top: 2px; + right: -26px; + width: 13px; + height: 13px; + border: 1px solid #c1c1c1; + border-radius: 50%; + } +} diff --git a/client/app/assets/less/inc/popover.less b/client/app/assets/less/inc/popover.less index 5fcad7089..12af4b959 100755 --- a/client/app/assets/less/inc/popover.less +++ b/client/app/assets/less/inc/popover.less @@ -1,22 +1,22 @@ -.popover { - box-shadow: fade(@redash-gray, 25%) 0px 0px 15px 0px; -} - -.popover-title { - border-bottom: 0; - padding: 15px; - font-size: 12px; - text-transform: uppercase; - - & + .popover-content { - padding-top: 0; - } -} - -.popover-content { - padding: 15px; - - p { - margin-bottom: 0; - } -} \ No newline at end of file +.popover { + box-shadow: fade(@redash-gray, 25%) 0px 0px 15px 0px; +} + +.popover-title { + border-bottom: 0; + padding: 15px; + font-size: 12px; + text-transform: uppercase; + + & + .popover-content { + padding-top: 0; + } +} + +.popover-content { + padding: 15px; + + p { + margin-bottom: 0; + } +} diff --git a/client/app/assets/less/inc/pricing-table.less b/client/app/assets/less/inc/pricing-table.less index 175512180..10d87b6fc 100755 --- a/client/app/assets/less/inc/pricing-table.less +++ b/client/app/assets/less/inc/pricing-table.less @@ -1,73 +1,72 @@ -.pricing-table { - margin-top: 50px; -} - -.pt-inner { - text-align: center; - - .pti-header { - padding: 45px 10px 70px; - color: #fff; - position: relative; - margin-bottom: 15px; - - - & > h2 { - margin: 0; - line-height: 100%; - color: #fff; - font-weight: 100; - font-size: 50px; - - small { - color: #fff; - letter-spacing: 0; - vertical-align: top; - font-size: 16px; - font-weight: 100; - } - } - - .ptih-title { - background-color: rgba(0, 0, 0, 0.1); - padding: 8px 10px 9px; - text-transform: uppercase; - margin: 0 -10px; - position: absolute; - width: 100%; - bottom: 0; - } - } - - .pti-body { - padding: 0 23px; - - .ptib-item { - padding: 15px 0; - font-weight: 400; - - &:not(:last-child) { - border-bottom: 1px solid #eee; - } - } - } - - .pti-footer { - padding: 10px 20px 30px; - - & > a { - width: 60px; - height: 60px; - border-radius: 50%; - text-align: center; - color: #fff; - display: inline-block; - line-height: 60px; - font-size: 30px; - - &:hover { - .opacity(0.85); - } - } - } -} \ No newline at end of file +.pricing-table { + margin-top: 50px; +} + +.pt-inner { + text-align: center; + + .pti-header { + padding: 45px 10px 70px; + color: #fff; + position: relative; + margin-bottom: 15px; + + & > h2 { + margin: 0; + line-height: 100%; + color: #fff; + font-weight: 100; + font-size: 50px; + + small { + color: #fff; + letter-spacing: 0; + vertical-align: top; + font-size: 16px; + font-weight: 100; + } + } + + .ptih-title { + background-color: rgba(0, 0, 0, 0.1); + padding: 8px 10px 9px; + text-transform: uppercase; + margin: 0 -10px; + position: absolute; + width: 100%; + bottom: 0; + } + } + + .pti-body { + padding: 0 23px; + + .ptib-item { + padding: 15px 0; + font-weight: 400; + + &:not(:last-child) { + border-bottom: 1px solid #eee; + } + } + } + + .pti-footer { + padding: 10px 20px 30px; + + & > a { + width: 60px; + height: 60px; + border-radius: 50%; + text-align: center; + color: #fff; + display: inline-block; + line-height: 60px; + font-size: 30px; + + &:hover { + .opacity(0.85); + } + } + } +} diff --git a/client/app/assets/less/inc/print.less b/client/app/assets/less/inc/print.less index 7209ae5f0..33cc44b4a 100755 --- a/client/app/assets/less/inc/print.less +++ b/client/app/assets/less/inc/print.less @@ -1,46 +1,45 @@ -@media print { - @page { - margin: 0; - padding: 0; - size: auto; - } - - body, #content, .container { - margin: 0mm 0mm 0mm 0mm !important; - padding: 0mm !important; - } - - - #header, - #sidebar, - #chat, - .growl-animated, - [data-action="print"] { - display: none !important; - } - - - /* -------------------------------------------------------- - Invoice - -----------------------------------------------------------*/ - .invoice { - padding: 30px !important; - -webkit-print-color-adjust: exact !important; - - .card-header { - background: #eee !important; - padding: 20px; - margin-bottom: 20px; - margin: -60px -30px 25px -30px; - } - - - .page-header { - display: none; - } - - .highlight { - background: #eee !important; - } - } -} \ No newline at end of file +@media print { + @page { + margin: 0; + padding: 0; + size: auto; + } + + body, + #content, + .container { + margin: 0mm 0mm 0mm 0mm !important; + padding: 0mm !important; + } + + #header, + #sidebar, + #chat, + .growl-animated, + [data-action="print"] { + display: none !important; + } + + /* -------------------------------------------------------- + Invoice + -----------------------------------------------------------*/ + .invoice { + padding: 30px !important; + -webkit-print-color-adjust: exact !important; + + .card-header { + background: #eee !important; + padding: 20px; + margin-bottom: 20px; + margin: -60px -30px 25px -30px; + } + + .page-header { + display: none; + } + + .highlight { + background: #eee !important; + } + } +} diff --git a/client/app/assets/less/inc/profile.less b/client/app/assets/less/inc/profile.less index 8df3341ff..e2c545333 100755 --- a/client/app/assets/less/inc/profile.less +++ b/client/app/assets/less/inc/profile.less @@ -1,366 +1,366 @@ -#profile-main { - min-height: 500px; - position: relative; -} - -.pm-overview { - overflow: hidden !important; - - @media (min-width: 1200px) { - width: 300px; - } - - @media (min-width: @screen-sm-min) and (max-width: 1200px) { - width: 250px; - } - - @media (min-width: @screen-sm-min) { - position: absolute; - left: 0; - top: 0; - height: 100%; - background: #f8f8f8; - border-right: 1px solid #eee; - } - - @media (max-width: @screen-xs-max) { - width: 100%; - background: #333; - text-align: center; - } - - &:hover { - .pmop-edit { - .opacity(1); - color: #fff; - } - } -} - -.pm-body { - @media (min-width: 1200px) { - padding-left: 300px; - } - - @media (min-width: @screen-sm-min) and (max-width: 1200px) { - padding-left: 250px; - } - - @media (max-width: @screen-xs-max) { - padding-left: 0; - } -} - -.pmo-pic { - position: relative; - margin: 20px; - - img { - @media(min-width: @screen-sm-min) { - width: 100%; - border-radius: 2px 2px 0 0; - } - - @media(max-width: @screen-xs-max) { - width: 180px; - display: inline-block; - height: 180px; - border-radius: 50%; - border: 4px solid #fff; - } - } -} - -.pmo-stat { - border-radius: 0 0 2px 2px; - color: #fff; - text-align: center; - padding: 30px 5px 0; - - @media(min-width: @screen-sm-min) { - background: @amber; - padding-bottom: 15px; - } -} - -.pmop-edit { - position: absolute; - top: 0; - left: 0; - color: #fff; - background: rgba(0, 0, 0, 0.38); - text-align: center; - padding: 10px 10px 11px; - - &:hover { - background: rgba(0, 0, 0, 0.8); - } - - i { - font-size: 18px; - vertical-align: middle; - margin-top: -3px; - } - - @media (min-width: @screen-sm-min) { - width: 100%; - .opacity(0); - - i { - margin-right: 4px; - } - } -} - -.pmop-message { - position: absolute; - bottom: 27px; - left: 50%; - margin-left: -25px; - - .dropdown-menu { - padding: 5px 0 55px; - left: -90px; - width: 228px; - height: 150px; - top: -74px; - - textarea { - width: 100%; - height: 95px; - border: 0; - resize: none; - padding: 10px 19px; - } - - button { - position: absolute; - bottom: 5px; - left: 93px; - } - } -} - -.pmb-block { - margin-bottom: 20px; - - @media (min-width: 1200px) { - padding: 40px 42px 0; - } - - @media (max-width: 1199px) { - padding: 30px 20px 0; - } - - &:last-child { - margin-bottom: 50px; - } - - &.toggled { - .pmbb-edit { - display: block; - } - - .pmbb-view { - display: none; - } - } -} - -.pmbb-header { - margin-bottom: 25px; - position: relative; - - .actions { - position: absolute; - top: -2px; - right: 0; - } - - h2 { - margin: 0; - font-weight: 100; - font-size: 20px; - } -} - -.pmbb-edit { - position: relative; - z-index: 1; - display: none; -} - -.pmo-block { - padding: 25px; - - & > h2 { - font-size: 16px; - margin: 0 0 15px; - } -} - -.pmo-items { - .pmob-body { - padding: 0 10px; - } - - a { - display: block; - padding: 4px; - - img { - width: 100%; - } - } -} - -.pmopm-send { - background-color: #fff; - width: 50px; - height: 50px; - font-size: 24px; - line-height: 53px; - border-radius: 50%; - position: absolute; - color: #333; - bottom: -50px; - box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.16); - text-align: center; - - &:hover { - color: #000; - } -} - -.pmo-contact { - ul { - list-style: none; - margin: 0; - padding: 0; - - li { - position: relative; - padding: 8px 0 8px 35px; - - i { - font-size: 18px; - vertical-align: top; - line-height: 100%; - position: absolute; - left: 0; - width: 18px; - text-align: center; - color: #333; - } - } - } -} - -.pmo-map { - margin: 20px -21px -18px; - display: block; - - img { - width: 100%; - } -} - -.p-header { - position: relative; - margin: 0 -7px; - - .actions { - position: absolute; - top: -18px; - right: 0; - } -} - -.p-menu { - list-style: none; - padding: 0 8px; - margin: 0 0 30px; - - & > li { - display: inline-block; - vertical-align: top; - - & > a { - display: block; - padding: 5px 20px 5px 0; - font-weight: 500; - text-transform: uppercase; - font-size: 15px; - - & > i { - margin-right: 4px; - font-size: 20px; - vertical-align: middle; - margin-top: -5px; - } - } - - &:not(.active) > a { - color: #4285F4; - - &:hover { - color: #333; - } - } - - &.active > a { - color: #000; - } - } - - .pm-search { - @media(max-width: @screen-sm-max) { - margin: 20px 2px 30px; - display: block; - - input[type="text"] { - width: 100%; - border: 1px solid #ccc; - } - } - } - - .pms-inner { - margin: -2px 0 0; - position: relative; - top: -2px; - overflow: hidden; - white-space: nowrap; - - i { - vertical-align: top; - font-size: 20px; - line-height: 100%; - position: absolute; - left: 9px; - top: 8px; - color: #333; - } - - input[type="text"] { - height: 35px; - border-radius: 2px; - padding: 0 10px 0 40px; - - @media(min-width: @screen-sm-min) { - border: 1px solid #fff; - width: 50px; - background: transparent; - position: relative; - z-index: 1; - .transition(all); - .transition-duration(300ms); - - &:focus { - border-color: #DFDFDF; - width: 200px; - } - } - } - } -} \ No newline at end of file +#profile-main { + min-height: 500px; + position: relative; +} + +.pm-overview { + overflow: hidden !important; + + @media (min-width: 1200px) { + width: 300px; + } + + @media (min-width: @screen-sm-min) and (max-width: 1200px) { + width: 250px; + } + + @media (min-width: @screen-sm-min) { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: #f8f8f8; + border-right: 1px solid #eee; + } + + @media (max-width: @screen-xs-max) { + width: 100%; + background: #333; + text-align: center; + } + + &:hover { + .pmop-edit { + .opacity(1); + color: #fff; + } + } +} + +.pm-body { + @media (min-width: 1200px) { + padding-left: 300px; + } + + @media (min-width: @screen-sm-min) and (max-width: 1200px) { + padding-left: 250px; + } + + @media (max-width: @screen-xs-max) { + padding-left: 0; + } +} + +.pmo-pic { + position: relative; + margin: 20px; + + img { + @media (min-width: @screen-sm-min) { + width: 100%; + border-radius: 2px 2px 0 0; + } + + @media (max-width: @screen-xs-max) { + width: 180px; + display: inline-block; + height: 180px; + border-radius: 50%; + border: 4px solid #fff; + } + } +} + +.pmo-stat { + border-radius: 0 0 2px 2px; + color: #fff; + text-align: center; + padding: 30px 5px 0; + + @media (min-width: @screen-sm-min) { + background: @amber; + padding-bottom: 15px; + } +} + +.pmop-edit { + position: absolute; + top: 0; + left: 0; + color: #fff; + background: rgba(0, 0, 0, 0.38); + text-align: center; + padding: 10px 10px 11px; + + &:hover { + background: rgba(0, 0, 0, 0.8); + } + + i { + font-size: 18px; + vertical-align: middle; + margin-top: -3px; + } + + @media (min-width: @screen-sm-min) { + width: 100%; + .opacity(0); + + i { + margin-right: 4px; + } + } +} + +.pmop-message { + position: absolute; + bottom: 27px; + left: 50%; + margin-left: -25px; + + .dropdown-menu { + padding: 5px 0 55px; + left: -90px; + width: 228px; + height: 150px; + top: -74px; + + textarea { + width: 100%; + height: 95px; + border: 0; + resize: none; + padding: 10px 19px; + } + + button { + position: absolute; + bottom: 5px; + left: 93px; + } + } +} + +.pmb-block { + margin-bottom: 20px; + + @media (min-width: 1200px) { + padding: 40px 42px 0; + } + + @media (max-width: 1199px) { + padding: 30px 20px 0; + } + + &:last-child { + margin-bottom: 50px; + } + + &.toggled { + .pmbb-edit { + display: block; + } + + .pmbb-view { + display: none; + } + } +} + +.pmbb-header { + margin-bottom: 25px; + position: relative; + + .actions { + position: absolute; + top: -2px; + right: 0; + } + + h2 { + margin: 0; + font-weight: 100; + font-size: 20px; + } +} + +.pmbb-edit { + position: relative; + z-index: 1; + display: none; +} + +.pmo-block { + padding: 25px; + + & > h2 { + font-size: 16px; + margin: 0 0 15px; + } +} + +.pmo-items { + .pmob-body { + padding: 0 10px; + } + + a { + display: block; + padding: 4px; + + img { + width: 100%; + } + } +} + +.pmopm-send { + background-color: #fff; + width: 50px; + height: 50px; + font-size: 24px; + line-height: 53px; + border-radius: 50%; + position: absolute; + color: #333; + bottom: -50px; + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.16); + text-align: center; + + &:hover { + color: #000; + } +} + +.pmo-contact { + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + position: relative; + padding: 8px 0 8px 35px; + + i { + font-size: 18px; + vertical-align: top; + line-height: 100%; + position: absolute; + left: 0; + width: 18px; + text-align: center; + color: #333; + } + } + } +} + +.pmo-map { + margin: 20px -21px -18px; + display: block; + + img { + width: 100%; + } +} + +.p-header { + position: relative; + margin: 0 -7px; + + .actions { + position: absolute; + top: -18px; + right: 0; + } +} + +.p-menu { + list-style: none; + padding: 0 8px; + margin: 0 0 30px; + + & > li { + display: inline-block; + vertical-align: top; + + & > a { + display: block; + padding: 5px 20px 5px 0; + font-weight: 500; + text-transform: uppercase; + font-size: 15px; + + & > i { + margin-right: 4px; + font-size: 20px; + vertical-align: middle; + margin-top: -5px; + } + } + + &:not(.active) > a { + color: #4285f4; + + &:hover { + color: #333; + } + } + + &.active > a { + color: #000; + } + } + + .pm-search { + @media (max-width: @screen-sm-max) { + margin: 20px 2px 30px; + display: block; + + input[type="text"] { + width: 100%; + border: 1px solid #ccc; + } + } + } + + .pms-inner { + margin: -2px 0 0; + position: relative; + top: -2px; + overflow: hidden; + white-space: nowrap; + + i { + vertical-align: top; + font-size: 20px; + line-height: 100%; + position: absolute; + left: 9px; + top: 8px; + color: #333; + } + + input[type="text"] { + height: 35px; + border-radius: 2px; + padding: 0 10px 0 40px; + + @media (min-width: @screen-sm-min) { + border: 1px solid #fff; + width: 50px; + background: transparent; + position: relative; + z-index: 1; + .transition(all); + .transition-duration(300ms); + + &:focus { + border-color: #dfdfdf; + width: 200px; + } + } + } + } +} diff --git a/client/app/assets/less/inc/progress-bar.less b/client/app/assets/less/inc/progress-bar.less index 4fa86dc99..958c31251 100755 --- a/client/app/assets/less/inc/progress-bar.less +++ b/client/app/assets/less/inc/progress-bar.less @@ -1,10 +1,10 @@ -.progress { - box-shadow: none; - border-radius: 0; - height: 5px; - margin-bottom: 0; - - .progress-bar { - box-shadow: none; - } -} \ No newline at end of file +.progress { + box-shadow: none; + border-radius: 0; + height: 5px; + margin-bottom: 0; + + .progress-bar { + box-shadow: none; + } +} diff --git a/client/app/assets/less/inc/schema-browser.less b/client/app/assets/less/inc/schema-browser.less index 3f2e66f28..f00523975 100644 --- a/client/app/assets/less/inc/schema-browser.less +++ b/client/app/assets/less/inc/schema-browser.less @@ -1,102 +1,107 @@ -div.table-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: pointer; - padding: 2px 22px 2px 10px; - border-radius: @redash-radius; - position: relative; - height: 22px; - - .copy-to-editor { - display: none; - } - - &:hover { - background: fade(@redash-gray, 10%); - - .copy-to-editor { - display: flex; - } - } -} - .schema-container { height: 100%; z-index: 10; background-color: white; -} -.schema-browser { - overflow: hidden; - border: none; - padding-top: 10px; - position: relative; - height: 100%; - - .schema-loading-state { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - } - - .collapse.in { - background: transparent; - } - - .copy-to-editor { - color: fade(@redash-gray, 90%); - cursor: pointer; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 20px; - display: flex; - align-items: center; - justify-content: center; - } - - .table-open { - padding: 0 22px 0 26px; + .schema-browser { overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + border: none; + padding-top: 10px; position: relative; - height: 18px; + height: 100%; - .column-type { - color: fade(@text-color, 80%); - font-size: 10px; - margin-left: 2px; - text-transform: uppercase; + .schema-loading-state { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + } + + .collapse.in { + background: transparent; } .copy-to-editor { - display: none; + visibility: hidden; + color: fade(@redash-gray, 90%); + width: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: none; } - &:hover { - background: fade(@redash-gray, 10%); + .schema-list-item { + display: flex; + border-radius: @redash-radius; + height: 22px; + + .table-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + padding: 2px 22px 2px 10px; + } + + &:hover, + &:focus, + &:focus-within { + background: fade(@redash-gray, 10%); - .copy-to-editor { + .copy-to-editor { + visibility: visible; + } + } + } + + .table-open { + .table-open-item { display: flex; + height: 18px; + width: calc(100% - 22px); + padding-left: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: none; + + div:first-child { + flex: 1; + } + + .column-type { + color: fade(@text-color, 80%); + font-size: 10px; + margin-left: 2px; + text-transform: uppercase; + } + + &:hover, + &:focus, + &:focus-within { + background: fade(@redash-gray, 10%); + + .copy-to-editor { + visibility: visible; + } + } } } } -} -.schema-control { - display: flex; - flex-wrap: nowrap; - padding: 0; + .schema-control { + display: flex; + flex-wrap: nowrap; + padding: 0; - .ant-btn { - height: auto; + .ant-btn { + height: auto; + } } -} -.parameter-label { - display: block; + .parameter-label { + display: block; + } } diff --git a/client/app/assets/less/inc/sidebar.less b/client/app/assets/less/inc/sidebar.less index 9ff581fac..440c6bd49 100755 --- a/client/app/assets/less/inc/sidebar.less +++ b/client/app/assets/less/inc/sidebar.less @@ -1,265 +1,262 @@ -#sidebar { - background-color: @sidebar; - position: fixed; - left: 0; - top: @header-height; - z-index: 9; - height: ~"calc(100% - 62px)"; - - @media (min-width: (@screen-lg-min + 80px)), (max-width: (@screen-sm-min)) { - width: @sidebar-left-width; - overflow: auto; - } - - @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { - &:not(.toggled) { - width: @sidebar-left-mid-width; - overflow: visible; - } - - &.toggled { - width: @sidebar-left-width; - overflow: auto; - } - } - - @media (max-width: @screen-sm-min) { - display: none; - - &.toggled { - display: block; - z-index: 12; - } - } -} - - -/* -------------------------------------------------------- - Profile Menu ------------------------------------------------------------*/ -.sms-profile { - margin: 12px 0 10px; - - & > a { - padding: 15px; - display: block; - color: @color-dark; - - & > img { - width: 28px; - height: 28px; - border-radius: 50%; - float: left; - margin-right: 10px; - margin-top: 3px; - } - } -} - - -/* -------------------------------------------------------- - Sidebar Menu ------------------------------------------------------------*/ -.side-menu { - list-style: none; - padding: 0; - - a { - color: @color-dark; - } - - & > li { - width: 100%; - display: block; - - & > a { - display: block; - padding: 9px 10px 9px 16px; - position: relative; - white-space: nowrap; - .transition(color); - - & > .zmdi { - font-size: 13px; - width: 28px; - height: 28px; - border-radius: 50%; - background-color: #000; - line-height: 29px; - margin-right: 7px; - text-align: center; - } - - .label { - position: absolute; - top: 15px; - right: 12px; - } - } - - &.active > a, - &:hover > a { - color: #fff; - } - - &.active > a { - background: @sidebar-active-bg; - - .zmdi { - background: #2C313A; - color: #fff; - } - } - } -} - -.sm-sub { - position: relative; - - &:not(.active) { - & > ul { - display: none; - } - } - - & > ul { - position: relative; - width: 100%; - padding: 0 0 0 27px; - background: darken(@sidebar, 1.5%); - margin-bottom: 0; - border: 0; - list-style: none; - - &:before { - content: ""; - height: 100%; - width: 1px; - position: absolute; - background: #1f2229; - left: 30px; - top: 0; - } - - & > li { - & > a { - padding: 7px 18px 7px 28px; - font-size: 12px; - display: block; - position: relative; - white-space: nowrap; - .transition(color); - - &:hover { - color: #fff; - } - - &:before { - content: ""; - width: 8px; - height: 1px; - background: #22252d; - position: absolute; - left: 4px; - top: 14px; - } - } - - &.active > a { - color: #fff; - } - - &:first-child > a { - &:before { - top: 20px; - } - - padding-top: 13px; - } - - &:last-child > a { - padding-bottom: 13px; - } - } - } -} - - -/* -------------------------------------------------------- - Sidebar for mid size screens ------------------------------------------------------------*/ -@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { - #sidebar:not(.toggled) { - .side-menu > li { - & > a { - & > span { - position: absolute; - left: @sidebar-left-mid-width; - background-color: @sidebar-active-bg; - width: 180px; - padding: 14px 18px; - display: none; - text-transform: uppercase; - .animated(fadeIn, 300ms); - } - - .label { - display: none; - } - } - - &.sms-bottom > a > span { - bottom: 0; - } - - &:not(.sms-bottom) > a > span { - top: 0; - } - - &:hover { - & a > span { - display: block; - } - } - } - - .sm-sub { - & > ul { - display: none !important; - position: absolute; - left: @sidebar-left-mid-width; - width: 180px; - padding-left: 0; - .animated(fadeIn, 300ms); - - &:before { - display: none; - } - - & > li > a { - padding-left: 18px; - - &:before { - display: none; - } - } - } - - &:not(.sms-bottom) > ul { - top: 46px; - border-top: 1px solid lighten(@sidebar, 5%); - } - - &.sms-bottom > ul { - bottom: 46px; - border-bottom: 1px solid lighten(@sidebar, 5%); - } - - &:hover { - & > ul { - display: block !important; - } - } - } - } -} \ No newline at end of file +#sidebar { + background-color: @sidebar; + position: fixed; + left: 0; + top: @header-height; + z-index: 9; + height: ~"calc(100% - 62px)"; + + @media (min-width: (@screen-lg-min + 80px)), (max-width: (@screen-sm-min)) { + width: @sidebar-left-width; + overflow: auto; + } + + @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { + &:not(.toggled) { + width: @sidebar-left-mid-width; + overflow: visible; + } + + &.toggled { + width: @sidebar-left-width; + overflow: auto; + } + } + + @media (max-width: @screen-sm-min) { + display: none; + + &.toggled { + display: block; + z-index: 12; + } + } +} + +/* -------------------------------------------------------- + Profile Menu +-----------------------------------------------------------*/ +.sms-profile { + margin: 12px 0 10px; + + & > a { + padding: 15px; + display: block; + color: @color-dark; + + & > img { + width: 28px; + height: 28px; + border-radius: 50%; + float: left; + margin-right: 10px; + margin-top: 3px; + } + } +} + +/* -------------------------------------------------------- + Sidebar Menu +-----------------------------------------------------------*/ +.side-menu { + list-style: none; + padding: 0; + + a { + color: @color-dark; + } + + & > li { + width: 100%; + display: block; + + & > a { + display: block; + padding: 9px 10px 9px 16px; + position: relative; + white-space: nowrap; + .transition(color); + + & > .zmdi { + font-size: 13px; + width: 28px; + height: 28px; + border-radius: 50%; + background-color: #000; + line-height: 29px; + margin-right: 7px; + text-align: center; + } + + .label { + position: absolute; + top: 15px; + right: 12px; + } + } + + &.active > a, + &:hover > a { + color: #fff; + } + + &.active > a { + background: @sidebar-active-bg; + + .zmdi { + background: #2c313a; + color: #fff; + } + } + } +} + +.sm-sub { + position: relative; + + &:not(.active) { + & > ul { + display: none; + } + } + + & > ul { + position: relative; + width: 100%; + padding: 0 0 0 27px; + background: darken(@sidebar, 1.5%); + margin-bottom: 0; + border: 0; + list-style: none; + + &:before { + content: ""; + height: 100%; + width: 1px; + position: absolute; + background: #1f2229; + left: 30px; + top: 0; + } + + & > li { + & > a { + padding: 7px 18px 7px 28px; + font-size: 12px; + display: block; + position: relative; + white-space: nowrap; + .transition(color); + + &:hover { + color: #fff; + } + + &:before { + content: ""; + width: 8px; + height: 1px; + background: #22252d; + position: absolute; + left: 4px; + top: 14px; + } + } + + &.active > a { + color: #fff; + } + + &:first-child > a { + &:before { + top: 20px; + } + + padding-top: 13px; + } + + &:last-child > a { + padding-bottom: 13px; + } + } + } +} + +/* -------------------------------------------------------- + Sidebar for mid size screens +-----------------------------------------------------------*/ +@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { + #sidebar:not(.toggled) { + .side-menu > li { + & > a { + & > span { + position: absolute; + left: @sidebar-left-mid-width; + background-color: @sidebar-active-bg; + width: 180px; + padding: 14px 18px; + display: none; + text-transform: uppercase; + .animated(fadeIn, 300ms); + } + + .label { + display: none; + } + } + + &.sms-bottom > a > span { + bottom: 0; + } + + &:not(.sms-bottom) > a > span { + top: 0; + } + + &:hover { + & a > span { + display: block; + } + } + } + + .sm-sub { + & > ul { + display: none !important; + position: absolute; + left: @sidebar-left-mid-width; + width: 180px; + padding-left: 0; + .animated(fadeIn, 300ms); + + &:before { + display: none; + } + + & > li > a { + padding-left: 18px; + + &:before { + display: none; + } + } + } + + &:not(.sms-bottom) > ul { + top: 46px; + border-top: 1px solid lighten(@sidebar, 5%); + } + + &.sms-bottom > ul { + bottom: 46px; + border-bottom: 1px solid lighten(@sidebar, 5%); + } + + &:hover { + & > ul { + display: block !important; + } + } + } + } +} diff --git a/client/app/assets/less/inc/table.less b/client/app/assets/less/inc/table.less index 37ee1be9d..7017881d4 100755 --- a/client/app/assets/less/inc/table.less +++ b/client/app/assets/less/inc/table.less @@ -103,7 +103,7 @@ padding-top: 5px !important; } - .btn-favourite, + .btn-favorite, .btn-archive { font-size: 15px; } @@ -115,18 +115,23 @@ line-height: 1.7 !important; } -.btn-favourite { +.btn-favorite { color: #d4d4d4; transition: all 0.25s ease-in-out; + .fa-star { + color: @yellow-darker; + } + &:hover, &:focus { color: @yellow-darker; cursor: pointer; - } - .fa-star { - color: @yellow-darker; + .fa-star { + filter: saturate(75%); + opacity: 0.75; + } } } diff --git a/client/app/assets/less/inc/tile.less b/client/app/assets/less/inc/tile.less index d0a3617ce..e93ece9ee 100755 --- a/client/app/assets/less/inc/tile.less +++ b/client/app/assets/less/inc/tile.less @@ -1,105 +1,104 @@ -.tile { - background-color: #fff; - margin-bottom: @grid-gutter-width; - position: relative; - border-radius: 3px; - box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px; - - &[class*="bg-"] { - color: #fff; - } - - @media (max-width: @screen-sm-min) { - margin-bottom: @grid-gutter-width/2; - } -} -.tiled { - border-radius: 3px; - box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px; -} - -.t-header { - .th-title { - line-height: 100%; - } - - &:not(.th-alt) { - padding: 20px 23px; - - .th-title { - font-size: 17px; - font-weight: 400; - color: #333; - - small { - font-size: 12px; - color: #9C9C9C; - margin-top: 3px; - display: block; - } - } - } - - &.widget { - padding: 5px; - } - - - &.th-alt { - padding: 10px 15px 9px; - - .actions { - & > a { - color: #fff; - } - } - - &[class*="bg-"] { - .th-title { - color: #fff; - } - } - } - - .actions { - right: 0; - top: 0; - - & > a { - font-size: 24px; - line-height: 100%; - padding: 4px 10px 3px; - display: block; - } - - & > a:hover, - &.open > a { - background-color: rgba(0, 0, 0, 0.1); - } - } -} - -.t-header:not(.th-alt) { - padding: 15px; - - ul { - margin-bottom: 0; - line-height: 2.2; - } - } - -.tb-padding { - padding: 20px 23px 30px; -} - -.t-body a.actions { - font-size: 24px; - line-height: 100%; - padding: 4px 10px 3px; - display: block; -} - -.t-body a.actions:hover, -.t-body a.actions.open > a { - background-color: rgba(0, 0, 0, 0.1); -} +.tile { + background-color: #fff; + margin-bottom: @grid-gutter-width; + position: relative; + border-radius: 3px; + box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px; + + &[class*="bg-"] { + color: #fff; + } + + @media (max-width: @screen-sm-min) { + margin-bottom: @grid-gutter-width / 2; + } +} +.tiled { + border-radius: 3px; + box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px; +} + +.t-header { + .th-title { + line-height: 100%; + } + + &:not(.th-alt) { + padding: 20px 23px; + + .th-title { + font-size: 17px; + font-weight: 400; + color: #333; + + small { + font-size: 12px; + color: #9c9c9c; + margin-top: 3px; + display: block; + } + } + } + + &.widget { + padding: 5px; + } + + &.th-alt { + padding: 10px 15px 9px; + + .actions { + & > a { + color: #fff; + } + } + + &[class*="bg-"] { + .th-title { + color: #fff; + } + } + } + + .actions { + right: 0; + top: 0; + + & > a { + font-size: 24px; + line-height: 100%; + padding: 4px 10px 3px; + display: block; + } + + & > a:hover, + &.open > a { + background-color: rgba(0, 0, 0, 0.1); + } + } +} + +.t-header:not(.th-alt) { + padding: 15px; + + ul { + margin-bottom: 0; + line-height: 2.2; + } +} + +.tb-padding { + padding: 20px 23px 30px; +} + +.t-body a.actions { + font-size: 24px; + line-height: 100%; + padding: 4px 10px 3px; + display: block; +} + +.t-body a.actions:hover, +.t-body a.actions.open > a { + background-color: rgba(0, 0, 0, 0.1); +} diff --git a/client/app/assets/less/inc/tooltips.less b/client/app/assets/less/inc/tooltips.less index 1213aa55b..545827ad5 100755 --- a/client/app/assets/less/inc/tooltips.less +++ b/client/app/assets/less/inc/tooltips.less @@ -1,5 +1,5 @@ -.tooltip-inner { - border-radius: 1px; - padding: 5px 10px; - font-size: 12px; -} \ No newline at end of file +.tooltip-inner { + border-radius: 1px; + padding: 5px 10px; + font-size: 12px; +} diff --git a/client/app/assets/less/inc/variables.less b/client/app/assets/less/inc/variables.less index c454fb4bd..0649ac8b9 100755 --- a/client/app/assets/less/inc/variables.less +++ b/client/app/assets/less/inc/variables.less @@ -1,287 +1,266 @@ /* -------------------------------------------------------- Paths -----------------------------------------------------------*/ -@imgpath: ~'../img'; -@fontpath: ~'../fonts'; - +@imgpath: ~"../img"; +@fontpath: ~"../fonts"; /* -------------------------------------------------------- Container -----------------------------------------------------------*/ -@container-tablet: 100%; -@container-desktop: 100%; -@container-large-desktop: 100%; - +@container-tablet: 100%; +@container-desktop: 100%; +@container-large-desktop: 100%; /* -------------------------------------------------------- Template Variables -----------------------------------------------------------*/ -@header-height: 60px; -@sidebar-left-width: 240px; -@sidebar-left-mid-width: 64px; -@logo-width: @sidebar-left-width; -@logo-height: @header-height; -@boxed-width: 1170px; -@body-bg: #edecec; -@spacing: 15px; -@redash-radius: 3px; - +@header-height: 60px; +@sidebar-left-width: 240px; +@sidebar-left-mid-width: 64px; +@logo-width: @sidebar-left-width; +@logo-height: @header-height; +@boxed-width: 1170px; +@body-bg: #edecec; +@spacing: 15px; +@redash-radius: 3px; /* -------------------------------------------------------- Branding -----------------------------------------------------------*/ -@brand-bg: #191C22; -@sidebar: @brand-bg; -@sidebar-active-bg: #121419; -@color-dark: #9BA1B1; - +@brand-bg: #191c22; +@sidebar: @brand-bg; +@sidebar-active-bg: #121419; +@color-dark: #9ba1b1; /* -------------------------------------------------------- Font -----------------------------------------------------------*/ -@font-icon: 'Material-Design-Iconic-Font'; -@font-family-sans-serif: 'Roboto', sans-serif; -@redash-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; -@font-size-base: 13px; - +@font-icon: "Material-Design-Iconic-Font"; +@font-family-sans-serif: "Roboto", sans-serif; +@redash-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", + sans-serif; +@font-size-base: 13px; /* -------------------------------------------------------- Typograpgy -----------------------------------------------------------*/ -@text-color: #160f66; -@link: #160f66; -@link-hover-decoration: none; -@headings-color: #333; - +@text-color: #767676; +@link: #02a4c4; +@link-hover-decoration: none; +@headings-color: #333; /* -------------------------------------------------------- Form -----------------------------------------------------------*/ -@input-color: #595959; -@input-color-placeholder: #b4b4b4; -@input-border: #e8e8e8; -@input-border-radius: 0; -@input-border-radius-large: 0px; -@redash-input-radius: 2px; -@input-height-large: 40px; -@input-height-base: 35px; -@input-height-small: 30px; -@input-border-focus: #79c2ff; -@input-group-addon-bg: @light-gray; +@input-color: #595959; +@input-color-placeholder: #b4b4b4; +@input-border: #e8e8e8; +@input-border-radius: 0; +@input-border-radius-large: 0px; +@redash-input-radius: 2px; +@input-height-large: 40px; +@input-height-base: 35px; +@input-height-small: 30px; +@input-border-focus: #79c2ff; +@input-group-addon-bg: @light-gray; /* -------------------------------------------------------- Colors -----------------------------------------------------------*/ -@white: #ffffff; -@black: #000000; -@blue: #2196F3; -@red: #F44336; -@purple: #9C27B0; -@deeppurple: #673AB7; -@lightblue: #03A9F4; -@cyan: #00BCD4; -@teal: #009688; -@green: #4CAF50; -@lightgreen: #8BC34A; -@lime: #CDDC39; -@yellow: #FFEB3B; -@yellow-darker: #fbd208; -@amber: #FFC107; -@orange: #FF9800; -@deeporange: #FF5722; -@gray: #9E9E9E; -@bluegray: #607D8B; -@indigo: #3F51B5; -@pink: #E91E63; -@brown: #795548; -@light-gray: #FCFCFC; -@gray-light: #828282; -@ace: #f8f8f8; +@white: #ffffff; +@black: #000000; +@blue: #2196f3; +@red: #f44336; +@purple: #9c27b0; +@deeppurple: #673ab7; +@lightblue: #03a9f4; +@cyan: #00bcd4; +@teal: #009688; +@green: #4caf50; +@lightgreen: #8bc34a; +@lime: #cddc39; +@yellow: #ffeb3b; +@yellow-darker: #fbd208; +@amber: #ffc107; +@orange: #ff9800; +@deeporange: #ff5722; +@gray: #9e9e9e; +@bluegray: #607d8b; +@indigo: #3f51b5; +@pink: #e91e63; +@brown: #795548; +@light-gray: #fcfcfc; +@gray-light: #828282; +@ace: #f8f8f8; @redash-gray: rgba(102, 136, 153, 1); @redash-orange: rgba(255, 120, 100, 1); @redash-black: rgba(0, 0, 0, 1); @redash-yellow: rgba(252, 252, 161, 0.75); - /** Form States **/ -@state-success-text: @green; -@state-info-text: @blue; -@state-danger-text: lighten(@red, 5%); - +/** Form States **/ +@state-success-text: @green; +@state-info-text: @blue; +@state-danger-text: lighten(@red, 5%); /* -------------------------------------------------------- Alert -----------------------------------------------------------*/ -@alert-success-border: transparent; -@alert-info-border: transparent; -@alert-danger-border: transparent; -@alert-inverse-border: transparent; - -@alert-success-bg: fade(@green, 70%); -@alert-info-bg: fade(@blue, 70%); -@alert-danger-bg: fade(@red, 70%); -@alert-inverse-bg: #333; +@alert-success-border: transparent; +@alert-info-border: transparent; +@alert-danger-border: transparent; +@alert-inverse-border: transparent; -@alert-success-text: #fff; -@alert-info-text: #fff; -@alert-danger-text: #fff; -@alert-inverse-text: #fff; +@alert-success-bg: fade(@green, 70%); +@alert-info-bg: fade(@blue, 70%); +@alert-danger-bg: fade(@red, 70%); +@alert-inverse-bg: #333; +@alert-success-text: #fff; +@alert-info-text: #fff; +@alert-danger-text: #fff; +@alert-inverse-text: #fff; /* -------------------------------------------------------- Bootstrap Brands -----------------------------------------------------------*/ -@brand-default: #eee; -@brand-primary: @blue; -@brand-info: @cyan; -@brand-success: @green; -@brand-warning: @orange; -@brand-danger: @red; - +@brand-default: #eee; +@brand-primary: @blue; +@brand-info: @cyan; +@brand-success: @green; +@brand-warning: @orange; +@brand-danger: @red; /* -------------------------------------------------------- Border Radius -----------------------------------------------------------*/ -@border-radius-base: 2px; -@border-radius-large: 2px; -@border-radius-small: 2px; - +@border-radius-base: 2px; +@border-radius-large: 2px; +@border-radius-small: 2px; /* -------------------------------------------------------- Dropdown -----------------------------------------------------------*/ -@dropdown-fallback-border: transparent; -@dropdown-border: transparent; -@dropdown-divider-bg: ''; -@dropdown-link-hover-bg: rgba(0,0,0,0.075); -@dropdown-link-color: #333; -@dropdown-link-hover-color: #333; -@dropdown-link-disabled-color: #e4e4e4; -@dropdown-divider-bg: rgba(0,0,0,0.08); -@dropdown-link-active-color: #333; -@dropdown-link-active-bg: rgba(0, 0, 0, 0.075); -@zindex-dropdown: 9; -@dropdown-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +@dropdown-fallback-border: transparent; +@dropdown-border: transparent; +@dropdown-divider-bg: ""; +@dropdown-link-hover-bg: rgba(0, 0, 0, 0.075); +@dropdown-link-color: #333; +@dropdown-link-hover-color: #333; +@dropdown-link-disabled-color: #e4e4e4; +@dropdown-divider-bg: rgba(0, 0, 0, 0.08); +@dropdown-link-active-color: #333; +@dropdown-link-active-bg: rgba(0, 0, 0, 0.075); +@zindex-dropdown: 9; +@dropdown-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* -------------------------------------------------------- Page Header -----------------------------------------------------------*/ -@page-header-border-color: transparent; - +@page-header-border-color: transparent; /* -------------------------------------------------------- Buttons -----------------------------------------------------------*/ -@btn-default-border: @input-border; -@btn-font-weight: 400; - +@btn-default-border: @input-border; +@btn-font-weight: 400; /* -------------------------------------------------------- Tables -----------------------------------------------------------*/ -@table-bg: #fff; -@table-border-color: #f0f0f0; -@table-cell-padding: 10px; -@table-condensed-cell-padding: 7px; -@table-bg-accent: @light-gray; -@table-bg-active: #FFFCBE; -@table-bg-hover: lighten(@light-gray, 2%); - +@table-bg: #fff; +@table-border-color: #f0f0f0; +@table-cell-padding: 10px; +@table-condensed-cell-padding: 7px; +@table-bg-accent: @light-gray; +@table-bg-active: #fffcbe; +@table-bg-hover: lighten(@light-gray, 2%); /* -------------------------------------------------------- Pagination -----------------------------------------------------------*/ -@pagination-bg: #E2E2E2; -@pagination-border: #fff; -@pagination-color: #7E7E7E; -@pagination-active-bg: @lightblue; -@pagination-active-border: @pagination-border; -@pagination-disabled-bg: #E2E2E2; -@pagination-disabled-border: @pagination-border; -@pagination-hover-color: #333; -@pagination-hover-bg: #d7d7d7; -@pagination-hover-border: @pagination-border; - +@pagination-bg: #e2e2e2; +@pagination-border: #fff; +@pagination-color: #7e7e7e; +@pagination-active-bg: @lightblue; +@pagination-active-border: @pagination-border; +@pagination-disabled-bg: #e2e2e2; +@pagination-disabled-border: @pagination-border; +@pagination-hover-color: #333; +@pagination-hover-bg: #d7d7d7; +@pagination-hover-border: @pagination-border; /* -------------------------------------------------------- Thumbnail -----------------------------------------------------------*/ -@thumbnail-bg: #fff; -@thumbnail-border: #eee; +@thumbnail-bg: #fff; +@thumbnail-border: #eee; /* -------------------------------------------------------- Carousel -----------------------------------------------------------*/ -@carousel-caption-color: #fff; - +@carousel-caption-color: #fff; /* -------------------------------------------------------- Modal -----------------------------------------------------------*/ -@modal-content-fallback-border-color: transparent; -@modal-content-border-color: transparent; -@modal-backdrop-bg: #000; -@modal-header-border-color: transparent; -@modal-title-line-height: transparent; -@modal-footer-border-color: transparent; -@zindex-modal-background: 10; - +@modal-content-fallback-border-color: transparent; +@modal-content-border-color: transparent; +@modal-backdrop-bg: #000; +@modal-header-border-color: transparent; +@modal-title-line-height: transparent; +@modal-footer-border-color: transparent; +@zindex-modal-background: 10; /* -------------------------------------------------------- Tooltips -----------------------------------------------------------*/ -@tooltip-bg: #333; -@tooltip-opacity: 1; - +@tooltip-bg: #333; +@tooltip-opacity: 1; /* -------------------------------------------------------- Popobver -----------------------------------------------------------*/ -@zindex-popover: 9; -@popover-title-bg: #fff; -@popover-border-color: #fff; -@popover-fallback-border-color: #fff; - +@zindex-popover: 9; +@popover-title-bg: #fff; +@popover-border-color: #fff; +@popover-fallback-border-color: #fff; /* -------------------------------------------------------- Breacrumb -----------------------------------------------------------*/ -@breadcrumb-bg: transparent; -@breadcrumb-padding-horizontal: 20px; -@breadcrumb-active-color: #7c7c7c; - +@breadcrumb-bg: transparent; +@breadcrumb-padding-horizontal: 20px; +@breadcrumb-active-color: #7c7c7c; /* -------------------------------------------------------- Jumbotron -----------------------------------------------------------*/ -@jumbotron-bg: #F7F7F7; - +@jumbotron-bg: #f7f7f7; /* -------------------------------------------------------- List Group -----------------------------------------------------------*/ -@list-group-border: #f4f4f4; -@list-group-active-color: #000; -@list-group-active-bg: #f5f5f5; -@list-group-active-border: @list-group-border; -@list-group-disabled-color: #B5B4B4; -@list-group-disabled-bg: #fff; -@list-group-disabled-text-color: #B5B4B4; - +@list-group-border: #f4f4f4; +@list-group-active-color: #000; +@list-group-active-bg: #f5f5f5; +@list-group-active-border: @list-group-border; +@list-group-disabled-color: #b5b4b4; +@list-group-disabled-bg: #fff; +@list-group-disabled-text-color: #b5b4b4; /* -------------------------------------------------------- Badges -----------------------------------------------------------*/ -@badge-color: #fff; -@badge-bg: @brand-primary; -@badge-border-radius: 2px; -@badge-font-weight: 400; -@badge-active-color: #fff; -@badge-active-bg: @brand-primary; - +@badge-color: #fff; +@badge-bg: @brand-primary; +@badge-border-radius: 2px; +@badge-font-weight: 400; +@badge-active-color: #fff; +@badge-active-bg: @brand-primary; /* -------------------------------------------------------- Misc -----------------------------------------------------------*/ -@code-bg: transparent; -@tile-shadow: 0 1px 1px rgba(0,0,0,0.07); +@code-bg: transparent; +@tile-shadow: 0 1px 1px rgba(0, 0, 0, 0.07); diff --git a/client/app/assets/less/inc/visualizations/box.less b/client/app/assets/less/inc/visualizations/box.less index beb7a20b4..9bad140e2 100755 --- a/client/app/assets/less/inc/visualizations/box.less +++ b/client/app/assets/less/inc/visualizations/box.less @@ -1,45 +1,47 @@ -.box { - font: 10px sans-serif; - line, rect, circle { - fill: #fff; - stroke: #000; - stroke-width: 1.5px; - } - .center { - stroke-dasharray: 3, 3; - } - .outlier { - fill: none; - stroke: #000; - } -} - -.axis text { - font: 10px sans-serif; -} - -.axis path, -.axis line { - fill: none; - stroke: #000; - shape-rendering: crispEdges; -} - -.grid-background { - fill: #ddd; -} - -.grid path, -.grid line { - fill: none; - stroke: #fff; - shape-rendering: crispEdges; -} - -.grid .minor line { - stroke-opacity: .5; -} - -.grid text { - display: none; -} +.box { + font: 10px sans-serif; + line, + rect, + circle { + fill: #fff; + stroke: #000; + stroke-width: 1.5px; + } + .center { + stroke-dasharray: 3, 3; + } + .outlier { + fill: none; + stroke: #000; + } +} + +.axis text { + font: 10px sans-serif; +} + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.grid-background { + fill: #ddd; +} + +.grid path, +.grid line { + fill: none; + stroke: #fff; + shape-rendering: crispEdges; +} + +.grid .minor line { + stroke-opacity: 0.5; +} + +.grid text { + display: none; +} diff --git a/client/app/assets/less/inc/well.less b/client/app/assets/less/inc/well.less index ccc94f914..cb80eff8f 100755 --- a/client/app/assets/less/inc/well.less +++ b/client/app/assets/less/inc/well.less @@ -1,5 +1,5 @@ -.well { - border-radius: 0; - background: #fff; - box-shadow: none; -} \ No newline at end of file +.well { + border-radius: 0; + background: #fff; + box-shadow: none; +} diff --git a/client/app/assets/less/inc/widgets.less b/client/app/assets/less/inc/widgets.less index 9e4d8fa1c..d52aaefea 100755 --- a/client/app/assets/less/inc/widgets.less +++ b/client/app/assets/less/inc/widgets.less @@ -1,30 +1,30 @@ -/* -------------------------------------------------------- - User Signups ------------------------------------------------------------*/ -.rounded-thumbs { - padding: 15px 25px 0; -} - -.rt-item { - display: block; - padding-top: 10px; - padding-bottom: 10px; - - img { - width: 100%; - height: 100%; - border-radius: 50%; - } - - small { - .text-overflow(); - text-align: center; - display: block; - color: #777; - margin-top: 3px; - } - - &:hover { - background-color: @light-gray; - } -} +/* -------------------------------------------------------- + User Signups +-----------------------------------------------------------*/ +.rounded-thumbs { + padding: 15px 25px 0; +} + +.rt-item { + display: block; + padding-top: 10px; + padding-bottom: 10px; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + } + + small { + .text-overflow(); + text-align: center; + display: block; + color: #777; + margin-top: 3px; + } + + &:hover { + background-color: @light-gray; + } +} diff --git a/client/app/assets/less/redash/custom/buttons.less b/client/app/assets/less/redash/custom/buttons.less index 736b10d2f..b8a4957df 100644 --- a/client/app/assets/less/redash/custom/buttons.less +++ b/client/app/assets/less/redash/custom/buttons.less @@ -2,7 +2,9 @@ border: 1px solid #ff5900; background: #ff5900; color: #ffffff; - &:hover, &:active, &:focus { + &:hover, + &:active, + &:focus { border: 1px solid #ff5900; background: #f58548; color: #ffffff; diff --git a/client/app/assets/less/redash/custom/editor-yaml.less b/client/app/assets/less/redash/custom/editor-yaml.less index 240350003..65e66aabd 100644 --- a/client/app/assets/less/redash/custom/editor-yaml.less +++ b/client/app/assets/less/redash/custom/editor-yaml.less @@ -1,7 +1,8 @@ .editor-yaml { width: 100%; max-width: 800px; - .ant-btn-primary, .ant-btn { + .ant-btn-primary, + .ant-btn { float: right; margin-top: 3px; } diff --git a/client/app/assets/less/redash/redash-table.less b/client/app/assets/less/redash/redash-table.less index 1c03c457c..5808132bb 100644 --- a/client/app/assets/less/redash/redash-table.less +++ b/client/app/assets/less/redash/redash-table.less @@ -25,9 +25,8 @@ & > thead > tr, & > tbody > tr, & > tfoot > tr { - - & > th, & > td { - + & > th, + & > td { &:first-child { padding-left: 15px; } @@ -35,7 +34,6 @@ &:last-child { padding-right: 15px; } - } } @@ -54,7 +52,8 @@ border: 0; & > tbody > tr { - & > td, & > th { + & > td, + & > th { border-bottom: 0; border-left: 0; @@ -83,11 +82,9 @@ border: 0; } -.tile .table { - +.tile .table { & > thead:not([class*="bg-"]) > tr > th { border-top: 1px solid @table-border-color; - } } @@ -101,7 +98,6 @@ background-color: fade(@redash-gray, 3%) !important; } - .table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, @@ -111,7 +107,6 @@ vertical-align: middle; } - .table-condensed > tbody > tr > td { padding: 7px 10px; } @@ -119,4 +114,3 @@ .table-border { border: 1px solid rgb(240, 240, 240); } - diff --git a/client/app/assets/less/redash/report.less b/client/app/assets/less/redash/report.less index ac6da3551..0fb2b9a83 100644 --- a/client/app/assets/less/redash/report.less +++ b/client/app/assets/less/redash/report.less @@ -67,10 +67,6 @@ } } - .report-metadata.report-metadata-horizontal { - border-bottom: 1px solid #efefef; - } - .tile, .tiled { box-shadow: none; @@ -268,16 +264,13 @@ display: none; } -@media (min-width: 880px) { +// Smaller screens +@media (max-height: 600px) { .report-fullscreen { - .report-metadata.report-metadata-horizontal { - display: none; - } + height: 100vh; } } -// Smaller screens - @media (max-width: 880px) { .btn--showhide, .report-actions-menu .dropdown-toggle { @@ -336,14 +329,15 @@ visibility: visible; } } +// end of Smaller screens .data-source-box { height: fit-content; background: #ffffff; - min-width: 200px; + width: 130px; .ant-select { display: inline-block; - width: calc(100% - 37px); + width: calc(100% - 13px); } .icon { display: inline-block; @@ -382,7 +376,7 @@ box-shadow: 2px 8px 8px rgba(0, 0, 0, 0.15); .icon-ui { position: relative; - opacity: 0.50; + opacity: 0.5; top: 1px; } &:hover { @@ -399,11 +393,12 @@ .mobile-navbar-toggle-button { border-color: transparent !important; &.ant-dropdown-open { - opacity: 0.50; + opacity: 0.5; } } -.ant-btn-primary, .ant-btn-danger { +.ant-btn-primary, +.ant-btn-danger { svg path { fill: #ffffff !important; } diff --git a/client/app/assets/less/server.less b/client/app/assets/less/server.less index 783376820..a045ddb99 100644 --- a/client/app/assets/less/server.less +++ b/client/app/assets/less/server.less @@ -1,33 +1,34 @@ /** LESS Plugins **/ -@import 'inc/less-plugins/for'; +@import "inc/less-plugins/for"; /** Load Main Bootstrap LESS files **/ -@import '~bootstrap/less/bootstrap'; -@import '~material-design-iconic-font/dist/css/material-design-iconic-font.css'; - -@import 'inc/variables'; -@import 'inc/mixins'; -@import 'inc/font'; -@import 'inc/print'; - -@import 'inc/bootstrap-overrides'; -@import 'inc/base'; -@import 'inc/generics'; -@import 'inc/form'; -@import 'inc/button'; -@import 'inc/404'; -@import 'inc/ie-warning'; -@import 'inc/flex'; - -html, body { +@import "~bootstrap/less/bootstrap"; +@import "~material-design-iconic-font/dist/css/material-design-iconic-font.css"; + +@import "inc/variables"; +@import "inc/mixins"; +@import "inc/font"; +@import "inc/print"; + +@import "inc/bootstrap-overrides"; +@import "inc/base"; +@import "inc/generics"; +@import "inc/form"; +@import "inc/button"; +@import "inc/404"; +@import "inc/ie-warning"; +@import "inc/flex"; + +html, +body { height: 100%; margin: 0; padding: 0; - background: #F6F8F9; + background: #f6f8f9; } .signed-out { - + width: auto; } .logo { diff --git a/client/app/assets/less/stylesheet.less b/client/app/assets/less/stylesheet.less index 8a13840a0..e0a4dc773 100644 --- a/client/app/assets/less/stylesheet.less +++ b/client/app/assets/less/stylesheet.less @@ -1,16 +1,26 @@ @font-face { - font-family: 'UI font solid'; - src: url('../fonts/uifont/uifont-solid-webfont.woff2') format('woff2'), - url('../fonts/uifont/uifont-solid-webfont.woff') format('woff'); - font-weight: normal; - font-style: normal; - + font-family: "UI font solid"; + src: + url("../fonts/uifont/uifont-solid-webfont.woff2") format("woff2"), + url("../fonts/uifont/uifont-solid-webfont.woff") format("woff"); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'UI font'; - src: url('../fonts/uifont/uifont-line-webfont.woff2') format('woff2'), - url('../fonts/uifont/uifont-line-webfont.woff') format('woff'); - font-weight: normal; - font-style: normal; + font-family: "UI font"; + src: + url("../fonts/uifont/uifont-line-webfont.woff2") format("woff2"), + url("../fonts/uifont/uifont-line-webfont.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +// This is a workaround for the loading icon in Ant Design +.anticon.anticon-loading.anticon-spin { + padding-right: 0px !important; +} + +.ant-btn-loading-icon { + padding-right: 1rem !important; } diff --git a/client/app/assets/manifest.json b/client/app/assets/manifest.json index e209d89ce..627bf80fe 100644 --- a/client/app/assets/manifest.json +++ b/client/app/assets/manifest.json @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/static/images/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/images/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "", + "short_name": "", + "icons": [ + { + "src": "/static/images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/client/app/components/AceEditorInput.jsx b/client/app/components/AceEditorInput.jsx index 22b45bad7..fd564bf21 100644 --- a/client/app/components/AceEditorInput.jsx +++ b/client/app/components/AceEditorInput.jsx @@ -1,5 +1,6 @@ import React, { forwardRef } from "react"; import AceEditor from "react-ace"; +import PropTypes from "prop-types"; import "./AceEditorInput.less"; @@ -19,4 +20,10 @@ function AceEditorInput(props, ref) { ); } -export default forwardRef(AceEditorInput); +const ForwardedAceEditorInput = forwardRef(AceEditorInput); + +ForwardedAceEditorInput.propTypes = { + "data-test": PropTypes.string, +}; + +export default ForwardedAceEditorInput; diff --git a/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.jsx b/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.jsx index b7b6b2d63..a6f492b39 100644 --- a/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.jsx +++ b/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.jsx @@ -1,42 +1,89 @@ -import { first } from "lodash"; -import React, { useState } from "react"; +import React, { useMemo } from "react"; +import { first, includes } from "lodash"; +import PropTypes from "prop-types"; import Menu from "antd/lib/menu"; -import Tooltip from "antd/lib/tooltip"; +import Tooltip from "@/components/Tooltip"; +import Link from "@/components/Link"; +import PlainButton from "@/components/PlainButton"; import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog"; +import { useCurrentRoute } from "@/components/ApplicationArea/Router"; import { Auth, currentUser } from "@/services/auth"; import settingsMenu from "@/services/settingsMenu"; -import routes from "@/services/routes"; -import location from "@/services/location"; import logoUrl from "@/assets/images/report_icon_small.png"; import VersionInfo from "./VersionInfo"; + import "./DesktopNavbar.less"; -function NavbarSection({ inlineCollapsed, children, ...props }) { +function NavbarSection({ children, ...props }) { return ( - + {children} ); } +NavbarSection.propTypes = { + children: PropTypes.node, +}; + +function useNavbarActiveState() { + const currentRoute = useCurrentRoute(); + + return useMemo( + () => ({ + dashboards: includes( + [ + "Dashboards.List", + "Dashboards.Favorites", + "Dashboards.My", + "Dashboards.ViewOrEdit", + "Dashboards.LegacyViewOrEdit", + ], + currentRoute.id, + ), + queries: includes( + [ + "Queries.List", + "Queries.Favorites", + "Queries.Archived", + "Queries.My", + "Queries.View", + "Queries.New", + "Queries.Edit", + ], + currentRoute.id, + ), + dataSources: includes(["DataSources.List"], currentRoute.id), + alerts: includes( + ["Alerts.List", "Alerts.New", "Alerts.View", "Alerts.Edit"], + currentRoute.id, + ), + reports: includes( + ["Reports.List", "Reports.View", "Reports.Edit", "Reports.New"], + currentRoute.id, + ), + }), + [currentRoute.id], + ); +} + export default function DesktopNavbar() { - const [collapsed, setCollapsed] = useState(true); + const activeState = useNavbarActiveState(); const firstSettingsTab = first(settingsMenu.getAvailableItems()); - const headerBlock = routes.getRoute(location.path) ? routes.getRoute(location.path).headerBlock : {}; const canCreateQuery = currentUser.hasPermission("create_query"); const canCreateDashboard = currentUser.hasPermission("create_dashboard"); const canCreateAlert = currentUser.hasPermission("list_alerts"); - const handleDeepRefresh = (event) => { + const handleDeepRefresh = event => { event.stopPropagation(); localStorage.setItem("bypass_cache", true); window.location.reload(); - } + }; - const handleNewReportButton = (event) => { + const handleNewReportButton = event => { event.preventDefault(); window.location.hash = "#"; if (window.location.pathname !== "/reports/new") { @@ -44,165 +91,177 @@ export default function DesktopNavbar() { } else { window.location.reload(); } - } + }; return (
- - - Data reporter - + +
+ + Data reporter + +
- + {currentUser.hasPermission("list_dashboards") && ( - - - - - + + + + + )} {currentUser.hasPermission("view_query") && ( - - - - - + + + + + )} {currentUser.hasPermission("view_query") && ( - - - + + + - + )} {currentUser.hasPermission("list_alerts") && ( - - - + + + - + )} - - {(canCreateQuery || canCreateDashboard || canCreateAlert) && } + + {(canCreateQuery || canCreateDashboard || canCreateAlert) && ( + + )} {(canCreateQuery || canCreateDashboard || canCreateAlert) && ( - - - - - + + + - }> + } + > {canCreateQuery && ( - + New Query - + )} {canCreateQuery && ( - + New Report - + )} {canCreateDashboard && ( - CreateDashboardDialog.showModal()}> + CreateDashboardDialog.showModal()} + > New Dashboard - + )} {canCreateAlert && ( - + New Alert - + )} )} - - {/* - - - - */} + -
- -
- - }> + {currentUser.name} + + } + > - Profile + Profile {currentUser.hasPermission("super_admin") && ( - System Status + System Status )} - Auth.logout()}> + Auth.logout()}> Log out - + - +
- - - {currentUser.name} - -
- + - + diff --git a/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.less b/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.less index 2bdbf3f4e..801494e90 100644 --- a/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.less +++ b/client/app/components/ApplicationArea/ApplicationLayout/DesktopNavbar.less @@ -58,7 +58,7 @@ &.ant-menu-submenu-active, &:hover, &:active { - color: @text-color; + color: @text-color; } a, @@ -81,7 +81,7 @@ &:hover, &:active { - color: @text-color;; + color: @text-color; } &:after { @@ -114,8 +114,10 @@ // styles from Antd opacity: 1; - transition: opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), - margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + transition: + opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } } @@ -138,16 +140,17 @@ .desktop-navbar-report { background: #ffffff; - height: 60px; - line-height: 60px; + height: 50px; + line-height: 50px; padding: 0 15px; margin: 0 15px; border-radius: 0 0 5px 5px; - .desktop-navbar-profile-menu, .settings-menu { + .desktop-navbar-profile-menu, + .settings-menu { float: right; } .desktop-navbar-logo { - height: 60px; + height: 50px; line-height: 60px; padding: 0; margin: 0; @@ -188,7 +191,7 @@ .left-border { position: relative; &:after { - content: ''; + content: ""; position: absolute; left: 0; top: 20px; @@ -196,14 +199,17 @@ height: 20px; } } - .ant-menu-dark, .ant-menu-sub { + .ant-menu-dark, + .ant-menu-sub { background-color: transparent; color: @text-color; } .desktop-navbar-logo.ant-menu.ant-menu-inline-collapsed img { height: 33px; } - .ant-menu-item, .ant-menu-item-group-title, .ant-menu-item > a { + .ant-menu-item, + .ant-menu-item-group-title, + .ant-menu-item > a { color: @text-color; } .ant-menu-vertical > .ant-menu-item, @@ -214,7 +220,7 @@ .ant-menu-vertical-left > .ant-menu-submenu > .ant-menu-submenu-title, .ant-menu-vertical-right > .ant-menu-submenu > .ant-menu-submenu-title, .ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title { - height: 60px; + height: 50px; line-height: 65px; width: auto; padding: 0 8px !important; @@ -242,9 +248,14 @@ background: @dividerColor; } + .ant-menu-item-disabled { + color: @textColor!important; + opacity: 0.65; + } + .ant-menu-item { font-weight: 500; - color: @textColor; + color: @textColor!important; &:hover, &:active { @@ -273,13 +284,12 @@ &:hover, &:active { - color: @text-color; + color: @text-color; } } } } } - } .ant-menu-vertical .ant-menu-item:not(:last-child), @@ -289,7 +299,8 @@ margin-bottom: 0 !important; } -.ant-menu-dark, .ant-menu-sub { +.ant-menu-dark, +.ant-menu-sub { background-color: #ffffff !important; color: @text-color !important; } @@ -306,4 +317,3 @@ .ant-menu-root.ant-menu-inline { border: none; } - diff --git a/client/app/components/ApplicationArea/ApplicationLayout/MobileNavbar.jsx b/client/app/components/ApplicationArea/ApplicationLayout/MobileNavbar.jsx index c7d0c0bc2..f878902ec 100644 --- a/client/app/components/ApplicationArea/ApplicationLayout/MobileNavbar.jsx +++ b/client/app/components/ApplicationArea/ApplicationLayout/MobileNavbar.jsx @@ -2,9 +2,9 @@ import { first } from "lodash"; import React from "react"; import PropTypes from "prop-types"; import Button from "antd/lib/button"; -import Icon from "antd/lib/icon"; import Dropdown from "antd/lib/dropdown"; import Menu from "antd/lib/menu"; +import Link from "@/components/Link"; import { Auth, currentUser } from "@/services/auth"; import settingsMenu from "@/services/settingsMenu"; import logoUrl from "@/assets/images/report_icon_small.png"; @@ -18,9 +18,9 @@ export default function MobileNavbar({ getPopupContainer }) { return (
- - Data reporter - + + Redash +
+ {currentUser.hasPermission("list_dashboards") && ( - Dashboards + Dashboards )} {currentUser.hasPermission("view_query") && ( - Queries + Queries )} - {currentUser.hasPermission("view_query") && ( - - Reports + {currentUser.hasPermission("view_report") && ( + + Reports )} {currentUser.hasPermission("list_alerts") && ( - Alerts + Alerts )} - Edit Profile + Edit Profile {firstSettingsTab && ( - Settings + Settings )} {currentUser.hasPermission("super_admin") && ( - System Status + System Status )} {currentUser.hasPermission("super_admin") && } {/* eslint-disable-next-line react/jsx-no-target-blank */} - + Help - + Auth.logout()}> Log out - }> + } + >
diff --git a/client/app/components/ApplicationArea/ApplicationLayout/VersionInfo.jsx b/client/app/components/ApplicationArea/ApplicationLayout/VersionInfo.jsx index 177ce99ae..87c3f6b89 100644 --- a/client/app/components/ApplicationArea/ApplicationLayout/VersionInfo.jsx +++ b/client/app/components/ApplicationArea/ApplicationLayout/VersionInfo.jsx @@ -1,24 +1,33 @@ import React from "react"; +import Link from "@/components/Link"; import { clientConfig, currentUser } from "@/services/auth"; - -const frontendVersion = "dev"; +import frontendVersion from "@/version.json"; export default function VersionInfo() { return ( -
+
Version: {clientConfig.version} - {frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`} + {frontendVersion !== clientConfig.version && + ` (${frontendVersion.substring(0, 8)})`}
- {clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && ( -
- {/* eslint-disable react/jsx-no-target-blank */} - - Update Available - - -
- )} + {clientConfig.newVersionAvailable && + currentUser.hasPermission("super_admin") && ( +
+ + Update Available{" "} +