├── .browserslistrc ├── .dockerignore ├── .env.development ├── .eslintignore ├── .eslintrc.js ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── analysis.yml │ ├── deploy.yml │ ├── docker.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── VERSION ├── babel.config.js ├── cypress.json ├── docs └── images │ ├── alerta-webui-v7-beta1.png │ └── alerta-webui-v7.png ├── jest.config.js ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── CNAME ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-144x144.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── audio │ └── alert_high-intensity.ogg ├── config.json.example ├── favicon-128.png ├── favicon-16x16.png ├── favicon-196x196.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png └── mstile-70x70.png ├── scripts └── deploy.sh ├── src ├── App.vue ├── assets │ ├── css │ │ └── fonts.css │ ├── fonts │ │ ├── FontAwesome │ │ │ ├── LICENSE.txt │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.woff2 │ │ │ └── fa-solid-900.woff2 │ │ ├── MaterialIcons │ │ │ └── MaterialIcons-Regular.ttf │ │ ├── Roboto │ │ │ └── Roboto-Regular.ttf │ │ ├── Sintony │ │ │ ├── OFL.txt │ │ │ ├── Sintony-Bold.ttf │ │ │ └── Sintony-Regular.ttf │ │ └── SonsieOne │ │ │ └── SonsieOne-Logo.woff2 │ └── logo.png ├── common │ └── utils.ts ├── components │ ├── AlertActions.vue │ ├── AlertDetail.vue │ ├── AlertIndicator.vue │ ├── AlertList.vue │ ├── AlertListFilter.vue │ ├── ApiKeyList.vue │ ├── BlackoutList.vue │ ├── CustomerList.vue │ ├── GroupList.vue │ ├── HeartbeatList.vue │ ├── Manifest.vue │ ├── PermList.vue │ ├── Preferences.vue │ ├── Status.vue │ ├── UserList.vue │ ├── auth │ │ ├── ProfileMe.vue │ │ ├── UserConfirm.vue │ │ ├── UserForgot.vue │ │ ├── UserLogin.vue │ │ ├── UserLogout.vue │ │ ├── UserReset.vue │ │ └── UserSignup.vue │ ├── lib │ │ ├── Banner.vue │ │ ├── DateTime.vue │ │ ├── ListButtonAdd.vue │ │ └── Snackbar.vue │ └── reports │ │ ├── ReportFilter.vue │ │ ├── TopFlapping.vue │ │ ├── TopOffenders.vue │ │ └── TopStanding.vue ├── directives │ └── hasPerms.ts ├── filters │ ├── capitalize.ts │ ├── date.ts │ ├── days.ts │ ├── hhmmss.ts │ ├── shortId.ts │ ├── splitCaps.ts │ ├── timeago.ts │ └── until.ts ├── locales │ ├── de.js │ ├── en.js │ ├── fr.js │ └── tr.js ├── main.ts ├── plugins │ ├── analytics.ts │ ├── i18n.ts │ └── vuetify.ts ├── router.ts ├── services │ ├── api │ │ ├── alert.service.ts │ │ ├── auth.service.ts │ │ ├── blackout.service.ts │ │ ├── customer.service.ts │ │ ├── group.service.ts │ │ ├── heartbeat.service.ts │ │ ├── index.ts │ │ ├── interceptors.ts │ │ ├── key.service.ts │ │ ├── management.service.ts │ │ ├── perms.service.ts │ │ ├── user.service.ts │ │ └── userInfo.service.ts │ ├── auth.ts │ └── config.ts ├── shims-tsx.d.ts ├── shims-vue-authenticate.d.ts ├── shims-vue.d.ts ├── shims-vuetify.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── alerts.store.ts │ │ ├── auth.store.ts │ │ ├── blackouts.store.ts │ │ ├── config.store.ts │ │ ├── customers.store.ts │ │ ├── groups.store.ts │ │ ├── heartbeats.store.ts │ │ ├── keys.store.ts │ │ ├── management.store.ts │ │ ├── notifications.store.ts │ │ ├── perms.store.ts │ │ ├── preferences.store.ts │ │ ├── reports.store.ts │ │ └── users.store.ts ├── stylus │ └── main.styl └── views │ ├── About.vue │ ├── Alert.vue │ ├── Alerts.vue │ ├── ApiKeys.vue │ ├── Blackouts.vue │ ├── Confirm.vue │ ├── Customers.vue │ ├── Forgot.vue │ ├── Groups.vue │ ├── Heartbeats.vue │ ├── Login.vue │ ├── Logout.vue │ ├── Perms.vue │ ├── Profile.vue │ ├── Reports.vue │ ├── Reset.vue │ ├── Settings.vue │ ├── Signup.vue │ └── Users.vue ├── static.json ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ └── components │ ├── ApiKeyList.spec.ts │ └── common │ └── utils.spec.ts ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_ALERTA_ENDPOINT=http://api.local.alerta.io:8080 2 | VUE_APP_TRACKING_ID=UA-44644195-7 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true 6 | }, 7 | extends: [ 8 | "plugin:vue/recommended", 9 | "@vue/typescript" 10 | ], 11 | rules: { 12 | semi: ["error", "never"], 13 | "no-console": "warn", 14 | "no-debugger": "error", 15 | quotes: ["error", "single"], 16 | "vue/script-indent": "error", 17 | "vue/name-property-casing": ["error", "PascalCase"], 18 | "vue/component-name-in-template-casing": ["error", "kebab-case"], 19 | "vue/html-indent": ["error", 2], 20 | "vue/script-indent": ["error", 2], 21 | }, 22 | parserOptions: { 23 | parser: "typescript-eslint-parser" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint", "github"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 16 | "@typescript-eslint/no-require-imports": "error", 17 | "@typescript-eslint/array-type": "error", 18 | "@typescript-eslint/await-thenable": "error", 19 | "@typescript-eslint/ban-ts-comment": "error", 20 | "camelcase": "off", 21 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 22 | "@typescript-eslint/func-call-spacing": ["error", "never"], 23 | "@typescript-eslint/no-array-constructor": "error", 24 | "@typescript-eslint/no-empty-interface": "error", 25 | "@typescript-eslint/no-explicit-any": "error", 26 | "@typescript-eslint/no-extraneous-class": "error", 27 | "@typescript-eslint/no-for-in-array": "error", 28 | "@typescript-eslint/no-inferrable-types": "error", 29 | "@typescript-eslint/no-misused-new": "error", 30 | "@typescript-eslint/no-namespace": "error", 31 | "@typescript-eslint/no-non-null-assertion": "warn", 32 | "@typescript-eslint/consistent-type-assertions": "error", 33 | "@typescript-eslint/no-unnecessary-qualifier": "error", 34 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 35 | "@typescript-eslint/no-useless-constructor": "error", 36 | "@typescript-eslint/no-var-requires": "error", 37 | "@typescript-eslint/prefer-for-of": "warn", 38 | "@typescript-eslint/prefer-function-type": "warn", 39 | "@typescript-eslint/prefer-includes": "error", 40 | "@typescript-eslint/consistent-type-definitions": "error", 41 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 42 | "@typescript-eslint/promise-function-async": "error", 43 | "@typescript-eslint/require-array-sort-compare": "error", 44 | "@typescript-eslint/restrict-plus-operands": "error", 45 | "semi": "off", 46 | "@typescript-eslint/semi": ["error", "never"], 47 | "@typescript-eslint/type-annotation-spacing": "error", 48 | "@typescript-eslint/unbound-method": "error", 49 | "i18n-text/no-en": "off" 50 | }, 51 | "env": { 52 | "node": true, 53 | "es6": true, 54 | "jest/globals": true 55 | } 56 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: satterly 2 | -------------------------------------------------------------------------------- /.github/workflows/analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '40 22 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [ master, release/* ] 6 | pull_request: 7 | branches: [ master, release/* ] 8 | 9 | env: 10 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '14' 21 | - run: npm version 22 | - run: npm install 23 | - name: Lint 24 | id: lint 25 | run: npm run lint:nofix 26 | - name: Unit Test 27 | id: unit-test 28 | run: npm run test:unit 29 | - uses: act10ns/slack@v2 30 | with: 31 | status: ${{ job.status }} 32 | steps: ${{ toJson(steps) }} 33 | if: failure() 34 | 35 | deploy: 36 | needs: test 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-node@v4 42 | with: 43 | node-version: '14' 44 | - run: npm version 45 | - run: npm install 46 | - run: | 47 | echo BASE_URL=./ > .env 48 | echo VUE_APP_ALERTA_ENDPOINT=https://alerta-api.fly.dev >> .env 49 | echo VUE_APP_TRACKING_ID=UA-44644195-1 >> .env 50 | - name: Build 51 | id: build 52 | run: npm run build 53 | - name: Deploy 54 | id: deploy 55 | uses: peaceiris/actions-gh-pages@v3 56 | with: 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | publish_dir: ./dist 59 | - uses: act10ns/slack@v2 60 | with: 61 | status: ${{ job.status }} 62 | steps: ${{ toJson(steps) }} 63 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ master, release/* ] 6 | tags: [ '**' ] 7 | 8 | env: 9 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 10 | 11 | jobs: 12 | build: 13 | name: Build & Push 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | REPOSITORY_URL: ghcr.io 18 | IMAGE_NAME: ${{ github.repository_owner }}/alerta-webui 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: '14' 25 | - run: npm version 26 | - name: Build Image 27 | id: docker-build 28 | run: >- 29 | docker build 30 | -t $IMAGE_NAME 31 | -t $REPOSITORY_URL/$IMAGE_NAME:$(cat VERSION) 32 | -t $REPOSITORY_URL/$IMAGE_NAME:$(git rev-parse --short HEAD) 33 | -t $REPOSITORY_URL/$IMAGE_NAME:latest . 34 | - name: Docker Login 35 | uses: docker/login-action@v2 36 | with: 37 | registry: ${{ env.REPOSITORY_URL }} 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | - name: Publish Image 41 | id: docker-push 42 | run: docker push --all-tags $REPOSITORY_URL/$IMAGE_NAME 43 | 44 | - uses: act10ns/slack@v2 45 | with: 46 | status: ${{ job.status }} 47 | steps: ${{ toJson(steps) }} 48 | if: failure() 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | 7 | env: 8 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '14' 20 | - run: npm version 21 | - run: npm install 22 | - name: Lint 23 | id: lint 24 | run: npm run lint:nofix 25 | - name: Unit Test 26 | id: unit-test 27 | run: npm run test:unit 28 | 29 | - uses: act10ns/slack@v2 30 | with: 31 | status: ${{ job.status }} 32 | steps: ${{ toJson(steps) }} 33 | if: failure() 34 | 35 | release: 36 | name: Publish 37 | needs: test 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: '14' 45 | - run: npm version 46 | - run: npm install 47 | - name: Build artifacts 48 | id: build 49 | run: | 50 | npm run build 51 | zip alerta-webui.zip -r dist/* 52 | tar cvfz alerta-webui.tar.gz dist/* 53 | - name: Create Release 54 | id: create-release 55 | uses: actions/create-release@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | tag_name: ${{ github.ref }} 60 | release_name: Release ${{ github.ref }} 61 | draft: false 62 | prerelease: false 63 | - name: Upload Zip File 64 | id: upload-zip-file 65 | uses: softprops/action-gh-release@v2 66 | with: 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | files: ./alerta-webui.zip 69 | - name: Upload Compressed TAR File 70 | id: upload-tarball 71 | uses: softprops/action-gh-release@v2 72 | with: 73 | token: ${{ secrets.GITHUB_TOKEN }} 74 | files: ./alerta-webui.tar.gz 75 | 76 | - uses: act10ns/slack@v2 77 | with: 78 | status: ${{ job.status }} 79 | steps: ${{ toJson(steps) }} 80 | if: failure() 81 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | env: 9 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '14' 20 | - run: npm version 21 | - id: build 22 | run: npm install 23 | - id: format-check 24 | run: npm run format-check 25 | - id: lint 26 | run: npm run lint:nofix 27 | - uses: act10ns/slack@v2 28 | with: 29 | status: ${{ job.status }} 30 | if: failure() 31 | 32 | test: 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: '14' 40 | - run: npm version 41 | - run: npm install 42 | - id: unit-test 43 | run: npm run test:unit 44 | # - id: e2e-test 45 | # run: npm run test:e2e 46 | - uses: act10ns/slack@v2 47 | with: 48 | status: ${{ job.status }} 49 | steps: ${{ toJson(steps) }} 50 | if: failure() 51 | 52 | build: 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/setup-node@v4 58 | with: 59 | node-version: '14' 60 | - run: npm version 61 | - id: build 62 | run: | 63 | npm install 64 | npm run all 65 | - uses: act10ns/slack@v2 66 | with: 67 | status: ${{ job.status }} 68 | steps: ${{ toJson(steps) }} 69 | if: always() 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v8.4.0 (2021-01-05) 2 | 3 | ### Fix 4 | 5 | - cancel timer and refresh alerts when env tab changes (#439) 6 | - axios method type no longer string (#437) 7 | 8 | ### Refactor 9 | 10 | - alert detail navigation was confusing (#438) 11 | 12 | ### Perf 13 | 14 | - only keep alive 1 environment tab (#433) 15 | - lazy load alert details and indicators components, again (#432) 16 | - lazy load alert details and indicators components (#431) 17 | - conditionally render environment tab contents (#430) 18 | 19 | ## v8.3.3 (2021-01-02) 20 | 21 | ### Fix 22 | 23 | - use better link color for dark mode (#422) 24 | 25 | ### Feat 26 | 27 | - add icon in alert summary if note added to alert (#428) 28 | - blackout service drop-down and new values (#425) 29 | 30 | ## 8.3.2 (2020-12-13) 31 | 32 | ### Fix 33 | 34 | - add helpful error message if susupected CORS issue (#420) 35 | 36 | ## 8.3.1 (2020-12-12) 37 | 38 | ### Fix 39 | 40 | - add helpful error message if susupected CORS issue (#419) 41 | 42 | ## v8.3.0 (2020-12-12) 43 | 44 | ### Fix 45 | 46 | - add default empty queries to preferences 47 | - render clickable custom attribute if value an object/array (#415) 48 | - don't keep adding the same search if pinned (#414) 49 | - profile me button when customer views disabled 50 | - custom attribute rendering in alert details 51 | - **ui**: rawData lines should be single spaced 52 | 53 | ### Feat 54 | 55 | - **pref**: always show allowed environments (#417) 56 | - add "X-Request-ID" for tracing (#416) 57 | - **search**: save searches to user preferences (#412) 58 | - make some alert details clickable for query search (#411) 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:12-alpine as build-stage 3 | RUN apk add --no-cache git 4 | WORKDIR /app 5 | COPY package*.json ./ 6 | RUN npm install 7 | COPY . . 8 | RUN npm run build 9 | 10 | # production stage 11 | FROM nginx:stable-alpine as production-stage 12 | COPY --from=build-stage /app/dist /usr/share/nginx/html 13 | COPY nginx.conf /etc/nginx/nginx.conf 14 | EXPOSE 80 15 | CMD ["nginx", "-g", "daemon off;"] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Alerta Web UI 7.0 2 | ================= 3 | 4 | [![Actions Status](https://github.com/alerta/alerta-webui/workflows/CI%20Tests/badge.svg)](https://github.com/alerta/alerta-webui/actions) [![Slack chat](https://img.shields.io/badge/chat-on%20slack-blue?logo=slack)](https://slack.alerta.dev) 5 | 6 | Version 7.0 of the Alerta web UI is a [VueJS](https://vuejs.org/) web app. 7 | 8 | ![webui](/docs/images/alerta-webui-v7.png?raw=true&v=1) 9 | 10 | Installation 11 | ------------ 12 | 13 | To install the web console: 14 | 15 | $ wget https://github.com/alerta/alerta-webui/releases/latest/download/alerta-webui.tar.gz 16 | $ tar zxvf alerta-webui.tar.gz 17 | $ cd dist 18 | $ python3 -m http.server 8000 19 | 20 | >> browse to http://localhost:8000 21 | 22 | Configuration 23 | ------------- 24 | 25 | Most configuration will come from the Alerta API server. The minimum, 26 | and most common, configuration is simply to tell the web UI where the 27 | API server is located. 28 | 29 | Environment variables for some settings can be used at build time: 30 | 31 | $ export VUE_APP_ALERTA_ENDPOINT=https://alerta-api.example.com 32 | $ npm install 33 | $ npm run build 34 | 35 | or place a `config.json` configuration file in the `dist` directory 36 | for run time configuration: 37 | 38 | { 39 | "endpoint": "https://alerta-api.example.com" 40 | } 41 | 42 | Any setting from the API server can be overridden if included in 43 | the local `config.json` file. For a full list of supported settings 44 | see the web UI config settings in the [online docs][1]. 45 | 46 | [1]: https://docs.alerta.io/en/latest/webui.html#configuration-from-api-server 47 | 48 | As a special case, support for setting an OAuth Client ID using a 49 | build-time environment variable is possible but should not be be 50 | necessary for most deployments. 51 | 52 | $ export VUE_APP_CLIENT_ID=0ffe5d26-6c66-4871-a6fa-593d9fa972b1 53 | 54 | Quick Start 55 | ----------- 56 | 57 | A docker container that is built using the most recent master branch is 58 | available for download from Docker Hub. 59 | 60 | $ docker pull alerta/alerta-beta 61 | 62 | It can also be built locally using the `Dockerfile` in this repository. 63 | 64 | $ docker build -t alerta/alerta-beta . 65 | 66 | To run, create a `config.json` file and mount the file into the container 67 | 68 | $ echo '{"endpoint": "https://alerta-api.example.com"}' > config.json 69 | $ docker run -v "$PWD/config.json:/usr/share/nginx/html/config.json" \ 70 | -it -p 8000:80 --rm --name alerta-beta alerta/alerta-beta 71 | 72 | Note: Update the `CORS_ORIGINS` setting in the Alerta API server config 73 | to include the URL that the beta web console is hosted at otherwise 74 | the browser will throw "blocked by CORS policy" errors and not work. 75 | 76 | Deployment 77 | ---------- 78 | 79 | Since this is a static web app then a production deployment of Alerta web UI 80 | is simply a matter of downloading the release tarball and copying the `dist` 81 | directory to the a location that can be served via a web server or CDN. 82 | 83 | See the [VueJS platform guide][2] for more information. 84 | 85 | [2]: https://cli.vuejs.org/guide/deployment.html#general-guidelines 86 | 87 | Troubleshooting 88 | --------------- 89 | 90 | The two main issues with deployment in production involve CORS and HTML5 91 | history mode. 92 | 93 | ### Cross-origin Errors (CORS) ### 94 | 95 | All modern browsers restrict access of a web app running at one domain to 96 | resources at a different origin (domain). This mechanism is known as [CORS][3]. 97 | 98 | [3]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 99 | 100 | To ensure that the Alerta web app has permission to access the Alerta API 101 | at a different origin the web URL needs to be added to the `CORS_ORIGINS` 102 | settings for the API. 103 | 104 | See [API server configuration][4] for more details. 105 | 106 | [4]: https://docs.alerta.io/en/latest/configuration.html#cors-config 107 | 108 | ### HTML5 History Mode 109 | 110 | The web app uses [HTML5 history mode][4] so you must ensure to configure 111 | the web server or CDN correctly otherwise users will get `404` errors when 112 | accessing deep links such as `/alert/:id` directly in their browser. 113 | 114 | The fix is to provide a [catch-all fallback route][5] so that any URL that 115 | doesn't match a static asset will be handled by the web app and redirected. 116 | 117 | **Example using nginx** 118 | ``` 119 | location / { 120 | try_files $uri $uri/ /index.html; 121 | } 122 | ``` 123 | 124 | [5]: https://router.vuejs.org/guide/essentials/history-mode.html 125 | [6]: https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations 126 | 127 | Development 128 | ----------- 129 | 130 | Project setup 131 | ``` 132 | npm install 133 | ``` 134 | 135 | Compiles and hot-reloads for development 136 | ``` 137 | npm run serve 138 | ``` 139 | 140 | Compiles and minifies for production 141 | ``` 142 | npm run build 143 | ``` 144 | 145 | Tests 146 | ----- 147 | 148 | Run your tests 149 | ``` 150 | npm run test 151 | ``` 152 | 153 | Lints and fixes files 154 | ``` 155 | npm run lint 156 | ``` 157 | 158 | Run your end-to-end tests 159 | ``` 160 | npm run test:e2e 161 | ``` 162 | 163 | Run your unit tests 164 | ``` 165 | npm run test:unit 166 | ``` 167 | 168 | License 169 | ------- 170 | 171 | Alerta monitoring system and console 172 | Copyright 2019-2021 Nick Satterly 173 | 174 | Licensed under the Apache License, Version 2.0 (the "License"); 175 | you may not use this file except in compliance with the License. 176 | You may obtain a copy of the License at 177 | 178 | http://www.apache.org/licenses/LICENSE-2.0 179 | 180 | Unless required by applicable law or agreed to in writing, software 181 | distributed under the License is distributed on an "AS IS" BASIS, 182 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 183 | See the License for the specific language governing permissions and 184 | limitations under the License. 185 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 8.7.1 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/alerta-webui-v7-beta1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/docs/images/alerta-webui-v7-beta1.png -------------------------------------------------------------------------------- /docs/images/alerta-webui-v7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/docs/images/alerta-webui-v7.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.tsx?$": "ts-jest", 8 | '^.+\\.jsx?$': 'babel-jest', 9 | }, 10 | moduleNameMapper: { 11 | "^@/(.*)$": "/src/$1" 12 | }, 13 | snapshotSerializers: ["jest-serializer-vue"], 14 | testMatch: [ 15 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 16 | ], 17 | testURL: "http://localhost/" 18 | }; 19 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /tmp/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | #tcp_nopush on; 23 | 24 | keepalive_timeout 65; 25 | 26 | # Compression options: 27 | gzip on; 28 | gzip_disable "msie6"; 29 | 30 | gzip_vary on; 31 | gzip_proxied any; 32 | gzip_comp_level 6; 33 | gzip_buffers 16 8k; 34 | gzip_http_version 1.1; 35 | gzip_min_length 256; 36 | gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; 37 | 38 | server_tokens off; 39 | 40 | server { 41 | listen 80 default_server; 42 | listen [::]:80 default_server ipv6only=on; 43 | 44 | root /usr/share/nginx/html; 45 | 46 | index index.html; 47 | location / { 48 | try_files $uri $uri/ /index.html; 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alerta-webui", 3 | "version": "8.7.1", 4 | "private": true, 5 | "description": "Alerta web UI", 6 | "scripts": { 7 | "serve": "vue-cli-service serve --port 8000", 8 | "build": "vue-cli-service build", 9 | "format": "prettier --write '**/*.ts'", 10 | "format-check": "prettier --check '**/*.ts'", 11 | "lint": "vue-cli-service lint src tests", 12 | "lint:nofix": "vue-cli-service lint --no-fix src tests", 13 | "test:e2e": "vue-cli-service test:e2e", 14 | "test:unit": "vue-cli-service test:unit", 15 | "all": "npm run build && npm run format && npm run lint && npm run test:unit" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/alerta/alerta-webui.git" 20 | }, 21 | "keywords": [ 22 | "alerta", 23 | "monitoring", 24 | "webui" 25 | ], 26 | "author": "satterly", 27 | "license": "Apache-2.0", 28 | "dependencies": { 29 | "@alerta/vue-authenticate": "github:alerta/vue-authenticate", 30 | "acorn-dynamic-import": "^4.0.0", 31 | "axios": "^0.28.1", 32 | "export-to-csv": "^0.2.1", 33 | "lodash": "^4.17.21", 34 | "moment": "^2.30.1", 35 | "nunjucks": "^3.2.3", 36 | "typescript-eslint-parser": "^22.0.0", 37 | "vue": "^2.7.16", 38 | "vue-authenticate": "github:alerta/vue-authenticate", 39 | "vue-class-component": "^6.3.2", 40 | "vue-i18n": "^8.28.2", 41 | "vue-object-merge": "^0.1.8", 42 | "vue-property-decorator": "^7.3.0", 43 | "vue-router": "^3.6.5", 44 | "vuetify": "^1.5.24", 45 | "vuex": "^3.6.2", 46 | "vuex-router-sync": "^5.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/jest": "^23.3.14", 50 | "@vue/cli-plugin-babel": "^4.5.15", 51 | "@vue/cli-plugin-e2e-cypress": "^5.0.8", 52 | "@vue/cli-plugin-eslint": "^4.5.15", 53 | "@vue/cli-plugin-typescript": "^4.5.15", 54 | "@vue/cli-plugin-unit-jest": "^4.5.15", 55 | "@vue/cli-service": "^4.5.15", 56 | "@vue/eslint-config-typescript": "^3.2.1", 57 | "@vue/test-utils": "^1.2.2", 58 | "babel-core": "7.0.0-bridge.0", 59 | "babel-eslint": "^10.1.0", 60 | "eslint": "^5.16.0", 61 | "eslint-plugin-vue": "^5.2.3", 62 | "node-sass": "^7.0.0", 63 | "prettier": "^2.4.1", 64 | "sass-loader": "^7.3.1", 65 | "stylus": "^0.54.8", 66 | "stylus-loader": "^3.0.1", 67 | "ts-jest": "^23.10.5", 68 | "typescript": "^3.9.10", 69 | "vue-cli-plugin-vuetify": "^0.4.6", 70 | "vue-template-compiler": "^2.7.16", 71 | "vuetify-loader": "^1.7.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | try.alerta.io -------------------------------------------------------------------------------- /public/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/audio/alert_high-intensity.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/audio/alert_high-intensity.ogg -------------------------------------------------------------------------------- /public/config.json.example: -------------------------------------------------------------------------------- 1 | {"endpoint": "http://localhost:8080"} 2 | -------------------------------------------------------------------------------- /public/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon-128.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon-196x196.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Alerta 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/mstile-310x310.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/public/mstile-70x70.png -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$1" =~ ^(\-\?|\-h)$ ]] 4 | then 5 | echo "Usage: `basename $0`" 6 | exit 1 7 | fi 8 | 9 | CLOUDFRONT_DIST_ID=E36XO0IMHMRWCO 10 | HOSTED_ZONE_ID=Z2RNJ4H6FV67LG 11 | DOMAIN=try.alerta.io 12 | S3_HOSTED_ZONE_ID=Z1BKCTXD74EZPE 13 | AWS_DEFAULT_REGION=eu-west-1 14 | export AWS_DEFAULT_REGION 15 | 16 | TMP_CONFIG_JSON=/tmp/config.json.$$ 17 | TMP_INDEX_HTML=/tmp/index.html.$$ 18 | TMP_INPUT_JSON=/tmp/route53-change-resource-record-sets.json.$$ 19 | 20 | ##### BUILD ##### 21 | 22 | npm run build 23 | 24 | ##### S3 COPY ##### 25 | 26 | echo "# Copy to S3: LOCAL -> s3://${DOMAIN} ..." 27 | 28 | aws s3 mb s3://${DOMAIN} 29 | aws s3 sync ../dist s3://${DOMAIN} --acl public-read 30 | aws s3 website s3://${DOMAIN} --index-document index.html 31 | 32 | ##### APP CONFIG ##### 33 | 34 | echo "# Copy updated config.json to S3: config.json -> s3://${DOMAIN}/config.json ..." 35 | 36 | cat >${TMP_CONFIG_JSON} << EOF 37 | {"endpoint": "https://alerta-api.herokuapp.com"} 38 | EOF 39 | 40 | aws s3 cp ${TMP_CONFIG_JSON} s3://${DOMAIN}/config.json --acl public-read --content-type application/javascript 41 | rm ${TMP_CONFIG_JSON} 42 | 43 | ##### CloudFront ##### 44 | 45 | aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DIST_ID} --paths "/*" 46 | 47 | ##### Route53 ##### 48 | 49 | echo "# Alias Record on ROUTE53: http://${DOMAIN} -> http://${DOMAIN}.s3-website-${AWS_DEFAULT_REGION}.amazonaws.com ..." 50 | 51 | cat >${TMP_INPUT_JSON} << EOF 52 | { 53 | "Comment": "Alerta explorer", 54 | "Changes": [ 55 | { 56 | "Action": "UPSERT", 57 | "ResourceRecordSet": { 58 | "Name": "${DOMAIN}", 59 | "Type": "A", 60 | "AliasTarget": { 61 | "HostedZoneId": "${S3_HOSTED_ZONE_ID}", 62 | "DNSName": "s3-website-${AWS_DEFAULT_REGION}.amazonaws.com.", 63 | "EvaluateTargetHealth": false 64 | } 65 | } 66 | } 67 | ] 68 | } 69 | EOF 70 | 71 | aws route53 change-resource-record-sets --hosted-zone-id ${HOSTED_ZONE_ID} --change-batch file://${TMP_INPUT_JSON} 72 | rm ${TMP_INPUT_JSON} 73 | 74 | echo "# Done." 75 | -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license/free. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OFL license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/FontAwesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/FontAwesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/FontAwesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/MaterialIcons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/MaterialIcons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Sintony/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Eduardo Tunni (http://www.tipo.net.ar), 2 | with Reserved Font Name 'Sintony' 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/assets/fonts/Sintony/Sintony-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/Sintony/Sintony-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Sintony/Sintony-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/Sintony/Sintony-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/SonsieOne/SonsieOne-Logo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/fonts/SonsieOne/SonsieOne-Logo.woff2 -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alerta/alerta-webui/f81a02fc20084ae7a3a4a8367464126e4a459100/src/assets/logo.png -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | getAllowedScopes(scopes: string[], allScopes: string[]) { 3 | let derivedScopes: string[] = [] 4 | 5 | function expandScope(scope: string) { 6 | return allScopes.filter(s => s.startsWith(scope)) 7 | } 8 | 9 | for (let scope of scopes) { 10 | derivedScopes.push(...expandScope(scope)) 11 | if (scope.startsWith('admin')) { 12 | derivedScopes.push(...expandScope(scope.replace('admin', 'delete'))) 13 | derivedScopes.push(...expandScope(scope.replace('admin', 'write'))) 14 | derivedScopes.push(...expandScope(scope.replace('admin', 'read'))) 15 | } 16 | if (scope.startsWith('write')) { 17 | derivedScopes.push(...expandScope(scope.replace('write', 'read'))) 18 | } 19 | } 20 | return Array.from(new Set(derivedScopes)).sort() 21 | }, 22 | toHash(obj: object): string { 23 | return Object.entries(obj) 24 | .filter(x => !!x[1]) 25 | .reduce((a: string[], [k, v]) => a.concat(`${k}:${v}`), []) 26 | .join(';') 27 | }, 28 | fromHash(hash: string): object { 29 | let h = decodeURI(hash).substring(1) 30 | return h 31 | ? h 32 | .split(';') 33 | .map(x => x.split(':')) 34 | .reduce((a, [k, v]) => Object.assign(a, {[k]: v}), {}) 35 | : {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/AlertActions.vue: -------------------------------------------------------------------------------- 1 | 160 | 161 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /src/components/AlertIndicator.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 157 | 158 | 177 | -------------------------------------------------------------------------------- /src/components/CustomerList.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /src/components/HeartbeatList.vue: -------------------------------------------------------------------------------- 1 | 155 | 156 | 231 | 232 | 263 | -------------------------------------------------------------------------------- /src/components/Manifest.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 127 | 128 | 136 | -------------------------------------------------------------------------------- /src/components/Status.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/components/auth/ProfileMe.vue: -------------------------------------------------------------------------------- 1 | 190 | 191 | 250 | 251 | 256 | -------------------------------------------------------------------------------- /src/components/auth/UserConfirm.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/components/auth/UserForgot.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/components/auth/UserLogin.vue: -------------------------------------------------------------------------------- 1 | 139 | 140 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/components/auth/UserLogout.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/auth/UserReset.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/components/auth/UserSignup.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/components/lib/Banner.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/lib/DateTime.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/lib/ListButtonAdd.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /src/components/lib/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/reports/TopFlapping.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/components/reports/TopOffenders.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/components/reports/TopStanding.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/directives/hasPerms.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import {store} from '@/main' 4 | 5 | // v-has-perms.disable="write:keys" 6 | // v-has-perms="admin:users" (hide is default) 7 | 8 | export default Vue.directive('has-perms', function (el, binding) { 9 | let authRequired = store.getters.getConfig('auth_required') 10 | let allowReadonly = store.getters.getConfig('allow_readonly') 11 | let readonlyScopes = store.getters.getConfig('readonly_scopes') 12 | let authenticated = store.state.auth.isAuthenticated 13 | 14 | if (!authRequired) { 15 | return true 16 | } 17 | if (allowReadonly) { 18 | authenticated = true 19 | } 20 | if (!authenticated) { 21 | return false 22 | } 23 | 24 | // helper function 25 | function isInScope(want, have): Boolean { 26 | if (have.includes(want) || have.includes(want.split(':')[0])) { 27 | return true 28 | } else if (want.startsWith('read')) { 29 | return isInScope(want.replace('read', 'write'), have) 30 | } else if (want.startsWith('write')) { 31 | return isInScope(want.replace('write', 'admin'), have) 32 | } 33 | return false 34 | } 35 | 36 | let perm = binding.value 37 | let scopes = authenticated ? store.getters['auth/scopes'] : readonlyScopes 38 | let action = binding.modifiers.disable ? 'disable' : 'hide' 39 | 40 | if (!perm) { 41 | return false 42 | } 43 | 44 | if (!isInScope(perm, scopes)) { 45 | if (action === 'disable') { 46 | el.setAttribute('disabled', '') 47 | } else { 48 | el.style.display = 'none' 49 | } 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /src/filters/capitalize.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // See https://vuejs.org/v2/guide/filters.html 4 | 5 | export default Vue.filter('capitalize', function (value) { 6 | if (value == null) return '' 7 | value = value.toString() 8 | return value.charAt(0).toUpperCase() + value.slice(1) 9 | }) 10 | -------------------------------------------------------------------------------- /src/filters/date.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import Vue from 'vue' 3 | 4 | export default Vue.filter('date', function (value, mode = 'local', format = 'll') { 5 | if (value) { 6 | if (mode === 'utc') { 7 | return moment.utc(String(value)).format(format) 8 | } else { 9 | return moment.utc(String(value)).local().format(format) 10 | } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/filters/days.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import Vue from 'vue' 3 | 4 | export default Vue.filter('days', function (value) { 5 | function pad(s) { 6 | return ('0' + s).slice(-2) 7 | } 8 | if (value) { 9 | let duration = moment.duration(value, 'seconds') 10 | var seconds = pad(duration.seconds()) 11 | var minutes = pad(duration.minutes()) 12 | var hours = pad(duration.hours()) 13 | var days = Math.floor(duration.as('d')) 14 | return `${days} days ${hours}:${minutes}:${seconds}` 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/filters/hhmmss.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import Vue from 'vue' 3 | 4 | export default Vue.filter('hhmmss', function (value) { 5 | function pad(s) { 6 | return ('0' + s).slice(-2) 7 | } 8 | if (value) { 9 | let duration = moment.duration(value, 'seconds') 10 | let seconds = pad(duration.seconds()) 11 | let minutes = pad(duration.minutes()) 12 | let hours = Math.floor(duration.as('h')) 13 | return `${hours}:${minutes}:${seconds}` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/filters/shortId.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default Vue.filter('shortId', function (value) { 4 | if (value) { 5 | return String(value).substring(0, 8) 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /src/filters/splitCaps.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default Vue.filter('splitCaps', function (value) { 4 | if (value == null) return '' 5 | return value 6 | .toString() 7 | .replace(/([A-Z])/g, ' $1') 8 | .split(' ') 9 | .map(word => { 10 | return word.charAt(0).toUpperCase() + word.slice(1) 11 | }) 12 | .join(' ') 13 | }) 14 | -------------------------------------------------------------------------------- /src/filters/timeago.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import Vue from 'vue' 3 | 4 | export default Vue.filter('timeago', function (value) { 5 | if (value) { 6 | return moment(String(value)).fromNow() 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/filters/until.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import Vue from 'vue' 3 | 4 | export default Vue.filter('until', function (value) { 5 | if (value) { 6 | return moment(String(value)).fromNow() 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import bootstrap from './services/config' 2 | 3 | import Vue from 'vue' 4 | 5 | import {createStore} from './store' 6 | import {createRouter} from './router' 7 | import {sync} from 'vuex-router-sync' 8 | import axios from 'axios' 9 | import {makeStore} from '@/store/modules/auth.store' 10 | import {makeInterceptors} from '@/services/api/interceptors' 11 | import {vueAuth} from '@/services/auth' 12 | import GoogleAnalytics from '@/plugins/analytics' 13 | import i18n from '@/plugins/i18n' 14 | 15 | import '@/plugins/vuetify' 16 | import './stylus/main.styl' 17 | import App from './App.vue' 18 | 19 | import '@/directives/hasPerms' 20 | 21 | import '@/filters/capitalize' 22 | import '@/filters/date' 23 | import '@/filters/days' 24 | import '@/filters/hhmmss' 25 | import '@/filters/shortId' 26 | import '@/filters/splitCaps' 27 | import '@/filters/timeago' 28 | import '@/filters/until' 29 | 30 | export const store = createStore() 31 | 32 | bootstrap.getConfig().then(config => { 33 | const router = createRouter(config.base_path) 34 | 35 | Vue.prototype.$config = config 36 | store.dispatch('updateConfig', config) 37 | store.dispatch('alerts/setFilter', config.filter) 38 | store.registerModule('auth', makeStore(vueAuth(config))) 39 | axios.defaults.baseURL = config.endpoint 40 | 41 | const interceptors = makeInterceptors(router) 42 | axios.interceptors.request.use(interceptors.requestIdHeader, undefined) 43 | axios.interceptors.response.use(undefined, interceptors.interceptErrors) 44 | axios.interceptors.response.use(undefined, interceptors.redirectToLogin) 45 | 46 | Vue.use(GoogleAnalytics, { 47 | trackingId: config.tracking_id, 48 | router 49 | }) 50 | sync(store, router) 51 | 52 | new Vue({ 53 | router, 54 | store, 55 | i18n, 56 | render: (h: any) => h(App) 57 | }).$mount('#app') 58 | }) 59 | -------------------------------------------------------------------------------- /src/plugins/analytics.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | dataLayer: Array 4 | gtag: (...args: any[]) => void 5 | } 6 | } 7 | 8 | const GoogleAnalytics = { 9 | install(Vue, {trackingId, router}) { 10 | if (!trackingId) { 11 | Vue.prototype.$track = () => {} 12 | } else { 13 | const script = document.createElement('script') 14 | script.async = true 15 | script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}` 16 | let head: HTMLElement = document.head! 17 | head.appendChild(script) 18 | 19 | function gtag(...args: any[]) { 20 | const dataLayer = (window.dataLayer = window.dataLayer || []) 21 | dataLayer.push(arguments) 22 | } 23 | gtag('js', new Date()) 24 | gtag('config', trackingId) 25 | 26 | Vue.prototype.$track = function (action: string, params?: object) { 27 | gtag('event', action, params) 28 | } 29 | 30 | router.afterEach(to => { 31 | gtag('config', trackingId, {page_path: to.fullPath}) 32 | }) 33 | } 34 | } 35 | } 36 | 37 | export default GoogleAnalytics 38 | -------------------------------------------------------------------------------- /src/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | // import file language from @/locales 5 | import {en} from '@/locales/en' 6 | import {fr} from '@/locales/fr' 7 | import {de} from '@/locales/de' 8 | import {tr} from '@/locales/tr' 9 | 10 | Vue.use(VueI18n) 11 | 12 | const loadLocaleMessages = { 13 | en, 14 | fr, 15 | de, 16 | tr 17 | } 18 | 19 | // variable navigator language 20 | let language = (navigator.languages && navigator.languages[0]) || navigator.language 21 | 22 | if (language.length > 2) { 23 | language = language.split('-')[0] 24 | language = language.split('_')[0] 25 | } 26 | 27 | // variable i18n for translation 28 | const i18n = new VueI18n({ 29 | locale: language, 30 | fallbackLocale: 'en', // set fallback locale 31 | messages: loadLocaleMessages 32 | }) 33 | 34 | export default i18n 35 | -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import colors from 'vuetify/es5/util/colors' 4 | 5 | import 'vuetify/src/stylus/app.styl' 6 | 7 | Vue.use(Vuetify, { 8 | theme: { 9 | primary: '#3f51b5', 10 | secondary: '#2196f3', 11 | accent: '#ffa726' 12 | }, 13 | iconfont: 'md' 14 | }) 15 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter, {RouterOptions} from 'vue-router' 3 | 4 | import {store} from '@/main' 5 | 6 | import Alerts from './views/Alerts.vue' 7 | import Alert from './views/Alert.vue' 8 | 9 | Vue.use(VueRouter) 10 | 11 | export function createRouter(basePath): VueRouter { 12 | const router = new VueRouter({ 13 | mode: 'history', 14 | base: basePath || process.env.BASE_URL, 15 | routes: [ 16 | { 17 | path: '/alerts', 18 | name: 'alerts', 19 | component: Alerts, 20 | props: route => ({ 21 | query: route.query, 22 | isKiosk: route.query.kiosk, 23 | hash: route.hash 24 | }), 25 | meta: {title: 'Alerts', requiresAuth: true} 26 | }, 27 | { 28 | path: '/alert/:id', 29 | name: 'alert', 30 | component: Alert, 31 | props: true, 32 | meta: {title: 'Alert Detail', requiresAuth: true} 33 | }, 34 | { 35 | path: '/heartbeats', 36 | name: 'heartbeats', 37 | component: () => import(/* webpackChunkName: 'user' */ './views/Heartbeats.vue'), 38 | meta: {title: 'Heartbeats', requiresAuth: true} 39 | }, 40 | { 41 | path: '/users', 42 | name: 'users', 43 | component: () => import(/* webpackChunkName: 'admin' */ './views/Users.vue'), 44 | meta: {title: 'Users', requiresAuth: true} 45 | }, 46 | { 47 | path: '/groups', 48 | name: 'groups', 49 | component: () => import(/* webpackChunkName: 'admin' */ './views/Groups.vue'), 50 | meta: {title: 'Groups', requiresAuth: true} 51 | }, 52 | { 53 | path: '/customers', 54 | name: 'customers', 55 | component: () => import(/* webpackChunkName: 'admin' */ './views/Customers.vue'), 56 | meta: {title: 'Customers', requiresAuth: true} 57 | }, 58 | { 59 | path: '/blackouts', 60 | name: 'blackouts', 61 | component: () => import(/* webpackChunkName: 'user' */ './views/Blackouts.vue'), 62 | meta: {title: 'Blackouts', requiresAuth: true} 63 | }, 64 | { 65 | path: '/perms', 66 | name: 'perms', 67 | component: () => import(/* webpackChunkName: 'admin' */ './views/Perms.vue'), 68 | meta: {title: 'Permissions', requiresAuth: true} 69 | }, 70 | { 71 | path: '/keys', 72 | name: 'apiKeys', 73 | component: () => import(/* webpackChunkName: 'user' */ './views/ApiKeys.vue'), 74 | meta: {title: 'API Keys', requiresAuth: true} 75 | }, 76 | { 77 | path: '/reports', 78 | name: 'reports', 79 | component: () => import(/* webpackChunkName: 'user' */ './views/Reports.vue'), 80 | meta: {title: 'Reports', requiresAuth: true} 81 | }, 82 | { 83 | path: '/profile', 84 | name: 'profile', 85 | component: () => import(/* webpackChunkName: 'user' */ './views/Profile.vue'), 86 | meta: {title: 'Profile', requiresAuth: true} 87 | }, 88 | { 89 | path: '/settings', 90 | name: 'settings', 91 | component: () => import(/* webpackChunkName: 'user' */ './views/Settings.vue'), 92 | meta: {title: 'Settings', requiresAuth: true} 93 | }, 94 | { 95 | path: '/help', 96 | name: 'help', 97 | component: () => window.open('https://docs.alerta.io/?utm_source=app', '_blank') 98 | }, 99 | { 100 | path: '/about', 101 | name: 'about', 102 | component: () => import(/* webpackChunkName: 'user' */ './views/About.vue'), 103 | meta: {title: 'About', requiresAuth: true} 104 | }, 105 | { 106 | path: '/login', 107 | name: 'login', 108 | component: () => import(/* webpackChunkName: 'auth' */ './views/Login.vue'), 109 | meta: {title: 'Login'} 110 | }, 111 | { 112 | path: '/signup', 113 | name: 'signup', 114 | component: () => import(/* webpackChunkName: 'auth' */ './views/Signup.vue'), 115 | meta: {title: 'Sign Up'} 116 | }, 117 | { 118 | path: '/confirm/:token', 119 | name: 'confirm', 120 | component: () => import(/* webpackChunkName: 'auth' */ './views/Confirm.vue'), 121 | meta: {title: 'Confirm Email'} 122 | }, 123 | { 124 | path: '/forgot', 125 | name: 'forgot', 126 | component: () => import(/* webpackChunkName: 'auth' */ './views/Forgot.vue'), 127 | meta: {title: 'Forgot Password'} 128 | }, 129 | { 130 | path: '/reset/:token', 131 | name: 'reset', 132 | component: () => import(/* webpackChunkName: 'auth' */ './views/Reset.vue'), 133 | meta: {title: 'Reset Password'} 134 | }, 135 | { 136 | path: '/logout', 137 | name: 'logout', 138 | component: () => import(/* webpackChunkName: 'auth' */ './views/Logout.vue'), 139 | meta: {title: 'Logout'} 140 | }, 141 | { 142 | path: '*', 143 | redirect: to => { 144 | // redirect hashbang mode links to HTML5 mode links 145 | if (to.fullPath.substr(0, 3) === '/#/') { 146 | return {path: to.fullPath.substr(2), hash: ''} 147 | } 148 | return '/alerts' 149 | } 150 | } 151 | ] 152 | } as RouterOptions) 153 | 154 | // redirect users not logged in to /login if authentication enabled 155 | router.beforeEach((to, from, next) => { 156 | if (store.getters.getConfig('auth_required') && to.matched.some(record => record.meta.requiresAuth)) { 157 | if (!store.getters['auth/isLoggedIn'] && !store.getters.getConfig('allow_readonly')) { 158 | next({ 159 | path: '/login', 160 | query: {redirect: to.fullPath} 161 | }) 162 | } else { 163 | next() 164 | } 165 | } else { 166 | next() 167 | } 168 | }) 169 | 170 | router.beforeEach((to, from, next) => { 171 | if (to?.meta?.title) { 172 | document.title = to.meta.title + ' | Alerta' 173 | } 174 | next() 175 | }) 176 | 177 | router.beforeEach((to, from, next) => { 178 | let externalUrl = to.fullPath.replace('/', '') 179 | if (externalUrl.match(/^(http(s)?|ftp):\/\//)) { 180 | window.open(externalUrl, '_blank') 181 | } else { 182 | next() 183 | } 184 | }) 185 | 186 | return router 187 | } 188 | -------------------------------------------------------------------------------- /src/services/api/alert.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | import axios from 'axios' 3 | 4 | let queryInProgress 5 | 6 | export default { 7 | getAlert(alertId: string) { 8 | return api.get(`/alert/${alertId}`) 9 | }, 10 | setStatus(alertId: string, data: object) { 11 | return api.put(`/alert/${alertId}/status`, data) 12 | }, 13 | actionAlert(alertId: string, data: object) { 14 | return api.put(`/alert/${alertId}/action`, data) 15 | }, 16 | tagAlert(alertId: string, data: object) { 17 | return api.put(`/alert/${alertId}/tag`, data) 18 | }, 19 | untagAlert(alertId: string, data: object) { 20 | return api.put(`/alert/${alertId}/untag`, data) 21 | }, 22 | updateAttributes(alertId: string, attributes: object) { 23 | let data = { 24 | attributes: attributes 25 | } 26 | return api.put(`/alert/${alertId}/attributes`, data) 27 | }, 28 | addNote(alertId: string, data: object) { 29 | return api.put(`/alert/${alertId}/note`, data) 30 | }, 31 | getNotes(alertId: string) { 32 | return api.get(`/alert/${alertId}/notes`) 33 | }, 34 | updateNote(alertId: string, noteId: string, data: object) { 35 | return api.put(`/alert/${alertId}/note/${noteId}`, data) 36 | }, 37 | deleteNote(alertId: string, noteId: string) { 38 | return api.delete(`/alert/${alertId}/note/${noteId}`) 39 | }, 40 | getAlerts(query: object) { 41 | if (query && queryInProgress) { 42 | queryInProgress.cancel('Too many search requests. Cancelling current query.') 43 | } 44 | queryInProgress = axios.CancelToken.source() 45 | let config = { 46 | params: query, 47 | cancelToken: queryInProgress.token 48 | } 49 | return api.get('/alerts', config) 50 | }, 51 | getAlertHistory(query: object) { 52 | let config = { 53 | params: query 54 | } 55 | return api.get('/alerts/history', config) 56 | }, 57 | getCounts(query: object) { 58 | let config = { 59 | params: query 60 | } 61 | return api.get('/alerts/count', config) 62 | }, 63 | getTop10Count(query: object) { 64 | let config = { 65 | params: query 66 | } 67 | return api.get('/alerts/top10/count', config) 68 | }, 69 | getTop10Flapping(query: object) { 70 | let config = { 71 | params: query 72 | } 73 | return api.get('/alerts/top10/flapping', config) 74 | }, 75 | getTop10Standing(query: object) { 76 | let config = { 77 | params: query 78 | } 79 | return api.get('/alerts/top10/standing', config) 80 | }, 81 | 82 | deleteAlert(alertId: string) { 83 | return api.delete(`/alert/${alertId}`) 84 | }, 85 | 86 | getEnvironments(query: object) { 87 | let config = { 88 | params: query 89 | } 90 | return api.get('/environments', config) 91 | }, 92 | getServices(query: object) { 93 | let config = { 94 | params: query 95 | } 96 | return api.get('/services', config) 97 | }, 98 | getGroups(query: object) { 99 | let config = { 100 | params: query 101 | } 102 | return api.get('/alerts/groups', config) 103 | }, 104 | getTags(query: object) { 105 | let config = { 106 | params: query 107 | } 108 | return api.get('/alerts/tags', config) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/services/api/auth.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | confirm(token: string) { 5 | return api.post(`/auth/confirm/${token}`, {}) 6 | }, 7 | forgot(email: string) { 8 | let data = { 9 | email: email 10 | } 11 | return api.post('/auth/forgot', data) 12 | }, 13 | reset(token: string, password: string) { 14 | let data = { 15 | password: password 16 | } 17 | return api.post(`/auth/reset/${token}`, data) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/services/api/blackout.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createBlackout(data: object) { 5 | return api.post('/blackout', data) 6 | }, 7 | getBlackout(id: string) { 8 | return api.get(`/blackout/${id}`) 9 | }, 10 | getBlackouts(query: object) { 11 | let config = { 12 | params: query 13 | } 14 | return api.get('/blackouts', config) 15 | }, 16 | updateBlackout(id: string, data: object) { 17 | return api.put(`/blackout/${id}`, data) 18 | }, 19 | deleteBlackout(id: string) { 20 | return api.delete(`/blackout/${id}`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/services/api/customer.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createCustomer(data: object) { 5 | return api.post('/customer', data) 6 | }, 7 | getCustomer(id: string) { 8 | return api.get(`/customer/${id}`) 9 | }, 10 | getCustomers(query: object) { 11 | let config = { 12 | params: query 13 | } 14 | return api.get('/customers', config) 15 | }, 16 | updateCustomer(id: string, data: object) { 17 | return api.put(`/customer/${id}`, data) 18 | }, 19 | deleteCustomer(id: string) { 20 | return api.delete(`/customer/${id}`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/services/api/group.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createGroup(data: object) { 5 | return api.post('/group', data) 6 | }, 7 | getGroup(groupId: string) { 8 | return api.get(`/group/${groupId}`) 9 | }, 10 | getGroupUsers(groupId: string) { 11 | return api.get(`/group/${groupId}/users`) 12 | }, 13 | getGroups(query: object) { 14 | let config = { 15 | params: query 16 | } 17 | return api.get('/groups', config) 18 | }, 19 | updateGroup(groupId: string, data: object) { 20 | return api.put(`/group/${groupId}`, data) 21 | }, 22 | addUserToGroup(groupId: string, userId: string) { 23 | return api.put(`/group/${groupId}/user/${userId}`, {}) 24 | }, 25 | removeUserFromGroup(groupId: string, userId: string) { 26 | return api.delete(`/group/${groupId}/user/${userId}`, {}) 27 | }, 28 | deleteGroup(groupId: string) { 29 | return api.delete(`/group/${groupId}`) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/services/api/heartbeat.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | getHeartbeat(id: string) { 5 | return api.get(`/heartbeat/${id}`) 6 | }, 7 | getHeartbeats(query: object) { 8 | let config = { 9 | params: query 10 | } 11 | return api.get('/heartbeats', config) 12 | }, 13 | deleteHeartbeat(id: string) { 14 | return api.delete(`/heartbeat/${id}`) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/api/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import {AxiosRequestConfig, Method} from 'axios' 4 | 5 | const api = { 6 | get(url: string, config?: AxiosRequestConfig) { 7 | return this.request('GET', url, null, config) 8 | }, 9 | 10 | delete(url: string, config?: AxiosRequestConfig) { 11 | return this.request('DELETE', url, null, config) 12 | }, 13 | 14 | head(url: string, config?: AxiosRequestConfig) { 15 | return this.request('HEAD', url, null, config) 16 | }, 17 | 18 | post(url: string, data?: any, config?: AxiosRequestConfig) { 19 | return this.request('POST', url, data, config) 20 | }, 21 | 22 | put(url: string, data?: any, config?: AxiosRequestConfig) { 23 | return this.request('PUT', url, data, config) 24 | }, 25 | 26 | patch(url: string, data?: any, config?: AxiosRequestConfig) { 27 | return this.request('PATCH', url, data, config) 28 | }, 29 | 30 | request(method: Method, url: string, data?: any, config?: AxiosRequestConfig) { 31 | let t0 = performance.now() 32 | return axios.request({...config, url, method, data}).then(response => { 33 | let t1 = performance.now() 34 | Vue.prototype.$track('timing_complete', { 35 | name: method, 36 | event_category: 'API', 37 | event_label: url, 38 | value: Math.round(t1 - t0) 39 | }) 40 | return response.data 41 | }) 42 | } 43 | } 44 | 45 | export default api 46 | -------------------------------------------------------------------------------- /src/services/api/interceptors.ts: -------------------------------------------------------------------------------- 1 | import {store} from '@/main' 2 | import {v4 as uuidv4} from 'uuid' 3 | import axios from 'axios' 4 | 5 | export function makeInterceptors(router) { 6 | return { 7 | // add requestId 8 | requestIdHeader(config) { 9 | config.headers['X-Request-ID'] = uuidv4() 10 | return config 11 | }, 12 | 13 | // response handlers 14 | interceptErrors(error) { 15 | if (!error.response && !axios.isCancel(error)) { 16 | store.dispatch('notifications/error', Error('Problem connecting to Alerta API, retrying...')) 17 | } 18 | 19 | if (error.response) { 20 | store.dispatch('notifications/error', error.response.data) 21 | } 22 | return Promise.reject(error) 23 | }, 24 | 25 | // redirect to login if API rejects auth token 26 | redirectToLogin(error) { 27 | if (error.response && error.response.status === 401) { 28 | if (store.getters['auth/isLoggedIn']) { 29 | store.dispatch('auth/logout') 30 | } 31 | if (router.currentRoute.path != '/login') { 32 | router.replace({ 33 | path: '/login', 34 | query: {redirect: router.currentRoute.fullPath} 35 | }) 36 | } 37 | } 38 | return Promise.reject(error) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/services/api/key.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createKey(data: object) { 5 | return api.post('/key', data) 6 | }, 7 | getKey(key: string) { 8 | return api.get(`/key/${key}`) 9 | }, 10 | getKeys(query: object) { 11 | let config = { 12 | params: query 13 | } 14 | return api.get('/keys', config) 15 | }, 16 | updateKey(key: string, data: object) { 17 | return api.put(`/key/${key}`, data) 18 | }, 19 | deleteKey(key: string) { 20 | return api.delete(`/key/${key}`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/services/api/management.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | manifest() { 5 | return api.get('/management/manifest') 6 | }, 7 | healthcheck() { 8 | return api.get('/management/healthcheck') 9 | }, 10 | status() { 11 | return api.get('/management/status') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/services/api/perms.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createPerm(data: object) { 5 | return api.post('/perm', data) 6 | }, 7 | getPerms(query: object) { 8 | let config = { 9 | params: query 10 | } 11 | return api.get('/perms', config) 12 | }, 13 | updatePerm(id: string, data: object) { 14 | return api.put(`/perm/${id}`, data) 15 | }, 16 | deletePerm(id: string) { 17 | return api.delete(`/perm/${id}`) 18 | }, 19 | 20 | getScopes() { 21 | return api.get('/scopes') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/api/user.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | createUser(data: object) { 5 | return api.post('/user', data) 6 | }, 7 | getUser(userId: string) { 8 | return api.get(`/user/${userId}`) 9 | }, 10 | getUserAttributes(userId: string) { 11 | return api.get(`/user/${userId}/attributes`) 12 | }, 13 | getMeAttributes() { 14 | return api.get('/user/me/attributes') 15 | }, 16 | getUsers(query: object) { 17 | let config = { 18 | params: query 19 | } 20 | return api.get('/users', config) 21 | }, 22 | updateUser(userId: string, data: object) { 23 | return api.put(`/user/${userId}`, data) 24 | }, 25 | updateMe(data: object) { 26 | return api.put('/user/me', data) 27 | }, 28 | updateUserAttributes(userId: string, attributes: object) { 29 | let data = { 30 | attributes: attributes 31 | } 32 | return api.put(`/user/${userId}/attributes`, data) 33 | }, 34 | updateMeAttributes(attributes: object) { 35 | let data = { 36 | attributes: attributes 37 | } 38 | return api.put('/user/me/attributes', data) 39 | }, 40 | deleteUser(userId: string) { 41 | return api.delete(`/user/${userId}`) 42 | }, 43 | getGroups(userId: string) { 44 | return api.get(`/user/${userId}/groups`) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/api/userInfo.service.ts: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | 3 | export default { 4 | userInfo() { 5 | return api.get('/userinfo') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import VueAxios from 'vue-axios' 4 | import {VueAuthenticate} from '@alerta/vue-authenticate' 5 | import axios from 'axios' 6 | 7 | Vue.use(Vuex) 8 | Vue.use(VueAxios, axios) 9 | 10 | function getRedirectUri(path: string) { 11 | return window.location.origin + (path || '') 12 | } 13 | 14 | export function vueAuth(config) { 15 | let basePath = config.base_path || process.env.BASE_URL 16 | return new VueAuthenticate(Vue.prototype.$http, { 17 | tokenPath: 'token', 18 | tokenName: 'token', 19 | tokenPrefix: '', 20 | registerUrl: '/auth/signup', 21 | logoutUrl: '/auth/logout', 22 | storageType: 'localStorage', 23 | storageNamespace: 'auth', 24 | providers: { 25 | azure: { 26 | name: 'Azure Active Directory', 27 | url: '/auth/azure', 28 | clientId: config.client_id, 29 | authorizationEndpoint: `https://login.microsoftonline.com/${config.azure_tenant}/oauth2/v2.0/authorize`, 30 | redirectUri: getRedirectUri(basePath), 31 | requiredUrlParams: ['scope'], 32 | optionalUrlParams: ['display', 'state'], 33 | scope: 'openid+profile+email', 34 | display: 'popup', 35 | oauthType: '2.0', 36 | popupOptions: {width: 1020, height: 618}, 37 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 38 | }, 39 | cognito: { 40 | name: 'Amazon Cognito', 41 | url: '/auth/openid', 42 | clientId: config.client_id, 43 | authorizationEndpoint: `https://${config.cognito_domain}.auth.${config.aws_region}.amazoncognito.com/login`, 44 | redirectUri: getRedirectUri(basePath), 45 | requiredUrlParams: ['scope'], 46 | optionalUrlParams: ['display', 'state'], 47 | scope: 'openid+profile+email', 48 | display: 'popup', 49 | oauthType: '2.0', 50 | popupOptions: {width: 1020, height: 618}, 51 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 52 | }, 53 | github: { 54 | name: 'GitHub', 55 | url: '/auth/github', 56 | clientId: config.client_id, 57 | authorizationEndpoint: `${config.github_url}/login/oauth/authorize`, 58 | redirectUri: getRedirectUri(basePath), 59 | scope: ['user:email', 'read:org'] 60 | }, 61 | gitlab: { 62 | name: 'GitLab', 63 | url: '/auth/gitlab', 64 | clientId: config.client_id, 65 | authorizationEndpoint: `${config.gitlab_url}/oauth/authorize`, 66 | redirectUri: getRedirectUri(basePath), 67 | requiredUrlParams: ['scope'], 68 | optionalUrlParams: ['display', 'state'], 69 | scope: ['openid'], 70 | display: 'popup', 71 | oauthType: '2.0', 72 | popupOptions: {width: 1020, height: 618}, 73 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 74 | }, 75 | google: { 76 | name: 'Google', 77 | url: '/auth/google', 78 | clientId: config.client_id, 79 | redirectUri: getRedirectUri(basePath) 80 | }, 81 | keycloak: { 82 | name: 'Keycloak', 83 | url: '/auth/keycloak', 84 | clientId: config.client_id, 85 | authorizationEndpoint: `${config.keycloak_url}/auth/realms/${config.keycloak_realm}/protocol/openid-connect/auth`, 86 | redirectUri: getRedirectUri(basePath), 87 | requiredUrlParams: ['scope'], 88 | optionalUrlParams: ['display', 'state'], 89 | scope: 'openid+profile+email', 90 | display: 'popup', 91 | oauthType: '2.0', 92 | popupOptions: {width: 1020, height: 618}, 93 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 94 | }, 95 | openid: { 96 | name: 'OpenID', 97 | url: '/auth/openid', 98 | clientId: config.client_id, 99 | authorizationEndpoint: config.oidc_auth_url, 100 | redirectUri: getRedirectUri(basePath), 101 | requiredUrlParams: ['scope'], 102 | optionalUrlParams: ['display', 'state'], 103 | scope: 'openid+profile+email', 104 | display: 'popup', 105 | oauthType: '2.0', 106 | popupOptions: {width: 1020, height: 618}, 107 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 108 | }, 109 | pingfederate: { 110 | name: 'PingFederate', 111 | url: '/auth/pingfederate', 112 | clientId: config.client_id, 113 | authorizationEndpoint: config.pingfederate_url, 114 | redirectUri: getRedirectUri(basePath || '/'), 115 | requiredUrlParams: ['pfidpadapterid', 'scope'], 116 | scope: 'openid+profile+email', 117 | pfidpadapterid: 'kerberos', 118 | oauthType: '2.0' 119 | } 120 | } 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /src/services/config.ts: -------------------------------------------------------------------------------- 1 | import Axios, {AxiosResponse, AxiosInstance} from 'axios' 2 | 3 | class Config { 4 | private config: any = {} 5 | private envConfig: any = {} 6 | private localConfig: any = {} 7 | private remoteConfig: any = {} 8 | 9 | private $http: AxiosInstance 10 | 11 | constructor() { 12 | this.$http = Axios.create() 13 | } 14 | 15 | getConfig(): Promise { 16 | return this.getEnvConfig() 17 | .then(response => { 18 | return this.setEnvConfig(response) 19 | }) 20 | .then(() => { 21 | return this.getLocalConfig() 22 | }) 23 | .then(response => { 24 | return this.setLocalConfig(response) 25 | }) 26 | .then(response => { 27 | let endpoint = this.config.endpoint ? this.config.endpoint : 'http://localhost:8080' 28 | return this.getRemoteConfig(endpoint) 29 | }) 30 | .then(response => { 31 | return this.setRemoteConfig(response) 32 | }) 33 | .catch((error: any) => { 34 | console.log(error) 35 | throw error 36 | }) 37 | } 38 | 39 | getEnvConfig() { 40 | return new Promise((resolve, reject) => { 41 | let envConfig = {} 42 | if (process.env.VUE_APP_ALERTA_ENDPOINT) { 43 | envConfig['endpoint'] = process.env.VUE_APP_ALERTA_ENDPOINT 44 | } 45 | if (process.env.VUE_APP_CLIENT_ID) { 46 | envConfig['client_id'] = process.env.VUE_APP_CLIENT_ID 47 | } 48 | if (process.env.VUE_APP_TRACKING_ID) { 49 | envConfig['tracking_id'] = process.env.VUE_APP_TRACKING_ID 50 | } 51 | resolve(envConfig) 52 | }) 53 | } 54 | 55 | getLocalConfig() { 56 | const basePath = process.env.BASE_URL 57 | return this.$http 58 | .get(`${basePath}config.json`) 59 | .then(response => response.data) 60 | .catch((error: any) => { 61 | console.warn(error.message) 62 | }) 63 | } 64 | 65 | getRemoteConfig(endpoint: string) { 66 | return this.$http 67 | .get(`${endpoint}/config`) 68 | .then(response => response.data) 69 | .catch((error: any) => { 70 | alert( 71 | `ERROR: Failed to retrieve client config from Alerta API endpoint ${endpoint}/config.\n\n` + 72 | 'This could be due to the API not being available, or to a missing or invalid ' + 73 | 'config.json file. Please confirm a config.json file exists, contains an "endpoint" ' + 74 | 'setting and is in the same directory as the application index.html file.' 75 | ) 76 | throw error 77 | }) 78 | } 79 | 80 | mergeConfig() { 81 | return (this.config = { 82 | ...this.remoteConfig, 83 | ...this.localConfig, 84 | ...this.envConfig 85 | }) 86 | } 87 | 88 | setEnvConfig(data: any) { 89 | this.envConfig = data 90 | return this.mergeConfig() 91 | } 92 | 93 | setLocalConfig(data: any) { 94 | this.localConfig = data 95 | return this.mergeConfig() 96 | } 97 | 98 | setRemoteConfig(data: any) { 99 | this.remoteConfig = data 100 | return this.mergeConfig() 101 | } 102 | 103 | $get() { 104 | return this.config 105 | } 106 | } 107 | 108 | export default new Config() 109 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, {VNode} from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue-authenticate.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-authenticate' 2 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/shims-vuetify.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vuetify/lib' 2 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, {Store} from 'vuex' 3 | import config from './modules/config.store' 4 | import alerts from './modules/alerts.store' 5 | import heartbeats from './modules/heartbeats.store' 6 | import blackouts from './modules/blackouts.store' 7 | import users from './modules/users.store' 8 | import groups from './modules/groups.store' 9 | import perms from './modules/perms.store' 10 | import customers from './modules/customers.store' 11 | import keys from './modules/keys.store' 12 | import reports from './modules/reports.store' 13 | import prefs from './modules/preferences.store' 14 | import management from './modules/management.store' 15 | import notifications from './modules/notifications.store' 16 | 17 | Vue.use(Vuex) 18 | 19 | const debug = process.env.NODE_ENV !== 'production' 20 | 21 | const mutations = { 22 | SET_SETTING(state, {s, v}) { 23 | state[s] = v 24 | } 25 | } 26 | 27 | const actions = { 28 | set({commit}, [s, v]) { 29 | commit('SET_SETTING', {s, v}) 30 | } 31 | } 32 | 33 | export function createStore(): Store { 34 | return new Vuex.Store({ 35 | state: { 36 | multiselect: false, 37 | refresh: false 38 | }, 39 | mutations, 40 | actions, 41 | strict: debug, 42 | modules: { 43 | config, 44 | alerts, 45 | heartbeats, 46 | blackouts, 47 | users, 48 | groups, 49 | perms, 50 | customers, 51 | keys, 52 | reports, 53 | prefs, 54 | management, 55 | notifications 56 | } 57 | }) 58 | } 59 | 60 | // FIXME: types... 61 | export interface State { 62 | config?: any 63 | isKiosk: boolean 64 | isDark: boolean 65 | alerts?: any 66 | users?: any 67 | auth?: any 68 | } 69 | -------------------------------------------------------------------------------- /src/store/modules/auth.store.ts: -------------------------------------------------------------------------------- 1 | import AuthApi from '@/services/api/auth.service' 2 | 3 | export function makeStore(vueAuth) { 4 | return { 5 | namespaced: true, 6 | 7 | state: { 8 | isAuthenticated: vueAuth.isAuthenticated(), 9 | token: vueAuth.getToken(), 10 | payload: vueAuth.getPayload(), 11 | 12 | isSending: false 13 | }, 14 | 15 | mutations: { 16 | SET_AUTH(state, [token, payload]) { 17 | state.isAuthenticated = true 18 | state.token = token 19 | state.payload = payload 20 | }, 21 | RESET_AUTH(state) { 22 | state.isAuthenticated = false 23 | state.token = null 24 | state.payload = {} 25 | }, 26 | SET_SENDING(state) { 27 | state.isSending = true 28 | }, 29 | RESET_SENDING(state) { 30 | state.isSending = false 31 | } 32 | }, 33 | 34 | actions: { 35 | signup({commit, dispatch}, {name, email, password, text}) { 36 | commit('SET_SENDING') 37 | return vueAuth 38 | .register({ 39 | name, 40 | email, 41 | password, 42 | text 43 | }) 44 | .then(() => commit('SET_AUTH', [vueAuth.getToken(), vueAuth.getPayload()])) 45 | .then(() => dispatch('getUserPrefs', {}, {root: true})) 46 | .finally(() => commit('RESET_SENDING')) 47 | }, 48 | login({commit, dispatch}, credentials) { 49 | return vueAuth 50 | .login(credentials) 51 | .then(() => commit('SET_AUTH', [vueAuth.getToken(), vueAuth.getPayload()])) 52 | .then(() => dispatch('getUserPrefs', {}, {root: true})) 53 | .catch(error => { 54 | throw error 55 | }) 56 | }, 57 | authenticate({commit, dispatch}, provider) { 58 | return vueAuth 59 | .authenticate(provider) 60 | .then(() => commit('SET_AUTH', [vueAuth.getToken(), vueAuth.getPayload()])) 61 | .then(() => dispatch('getUserPrefs', {}, {root: true})) 62 | .catch(error => { 63 | throw error 64 | }) 65 | }, 66 | setToken({commit, dispatch}, token) { 67 | vueAuth.setToken(token) 68 | commit('SET_AUTH', [token, vueAuth.getPayload()]) 69 | dispatch('getUserPrefs', {}, {root: true}) 70 | }, 71 | confirm({commit}, token) { 72 | return AuthApi.confirm(token) 73 | }, 74 | forgot({commit}, email) { 75 | commit('SET_SENDING') 76 | return AuthApi.forgot(email).finally(() => commit('RESET_SENDING')) 77 | }, 78 | reset({commit}, [token, password]) { 79 | return AuthApi.reset(token, password) 80 | }, 81 | logout({commit}) { 82 | return vueAuth 83 | .logout() 84 | .then(response => { 85 | return response 86 | }) 87 | .finally(() => commit('RESET_AUTH')) 88 | } 89 | }, 90 | 91 | getters: { 92 | getOptions() { 93 | return vueAuth.options 94 | }, 95 | getPayload(state) { 96 | return state.payload 97 | }, 98 | isLoggedIn(state) { 99 | return state.isAuthenticated 100 | }, 101 | getUsername(state) { 102 | return state.payload && state.payload.preferred_username 103 | }, 104 | getAvatar(state) { 105 | return state.payload && state.payload.picture 106 | }, 107 | scopes(state) { 108 | return state.payload && state.payload.scope ? state.payload.scope.split(' ') : [] 109 | }, 110 | customers(state) { 111 | return state.payload.customers && state.payload.customers.length == 0 ? ['ALL (*)'] : state.payload.customers 112 | }, 113 | isAdmin(state, getters) { 114 | if (getters.isLoggedIn) { 115 | return getters.scopes.includes('admin') 116 | } 117 | return false 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/store/modules/blackouts.store.ts: -------------------------------------------------------------------------------- 1 | import BlackoutsApi from '@/services/api/blackout.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | isLoading: false, 7 | 8 | blackouts: [] 9 | } 10 | 11 | const mutations = { 12 | SET_LOADING(state) { 13 | state.isLoading = true 14 | }, 15 | SET_BLACKOUTS(state, blackouts) { 16 | state.isLoading = false 17 | state.blackouts = blackouts 18 | }, 19 | RESET_LOADING(state) { 20 | state.isLoading = false 21 | } 22 | } 23 | 24 | const actions = { 25 | getBlackouts({commit}) { 26 | commit('SET_LOADING') 27 | return BlackoutsApi.getBlackouts({}) 28 | .then(({blackouts}) => commit('SET_BLACKOUTS', blackouts)) 29 | .catch(() => commit('RESET_LOADING')) 30 | }, 31 | createBlackout({dispatch, commit}, blackout) { 32 | return BlackoutsApi.createBlackout(blackout).then(response => { 33 | dispatch('getBlackouts') 34 | }) 35 | }, 36 | updateBlackout({dispatch, commit}, [blackoutId, update]) { 37 | return BlackoutsApi.updateBlackout(blackoutId, update).then(response => { 38 | dispatch('getBlackouts') 39 | }) 40 | }, 41 | deleteBlackout({dispatch, commit}, blackoutId) { 42 | return BlackoutsApi.deleteBlackout(blackoutId).then(response => { 43 | dispatch('getBlackouts') 44 | }) 45 | } 46 | } 47 | 48 | const getters = { 49 | // 50 | } 51 | 52 | export default { 53 | namespaced, 54 | state, 55 | mutations, 56 | actions, 57 | getters 58 | } 59 | -------------------------------------------------------------------------------- /src/store/modules/config.store.ts: -------------------------------------------------------------------------------- 1 | import stateMerge from 'vue-object-merge' 2 | 3 | const state = { 4 | endpoint: 'http://local.alerta.io:8080', 5 | alarm_model: {}, // includes severity, colors and status maps 6 | 7 | auth_required: true, 8 | allow_readonly: false, 9 | readonly_scopes: ['read'], 10 | provider: 'basic', 11 | customer_views: false, 12 | signup_enabled: true, 13 | email_verification: false, 14 | 15 | client_id: null, 16 | github_url: 'https://github.com', 17 | gitlab_url: 'https://gitlab.com', 18 | keycloak_realm: null, 19 | keycloak_url: null, 20 | pingfederate_url: null, 21 | 22 | site_logo_url: '', 23 | 24 | severity: {}, // moved to alarm_model 25 | colors: {}, // moved to alarm_model 26 | clipboard_template: '', 27 | 28 | timeouts: {}, // includes alert, heartbeat, ack and shelve timeouts 29 | 30 | blackouts: {}, // include default duration 31 | 32 | dates: { 33 | longDate: 'ddd D MMM, YYYY HH:mm:ss.SSS Z', 34 | mediumDate: 'ddd D MMM HH:mm', 35 | shortTime: 'HH:mm' 36 | }, 37 | font: { 38 | 'font-family': '"Sintony", Arial, sans-serif', 39 | 'font-size': '13px', 40 | 'font-weight': 500 41 | }, 42 | audio: {}, 43 | columns: [], 44 | sort_by: ['severity', 'lastReceiveTime'], 45 | actions: [], 46 | filter: { 47 | text: null, 48 | environment: null, 49 | status: null, 50 | service: null, 51 | group: null, 52 | dateRange: [null, null] 53 | }, 54 | 55 | tracking_id: null, 56 | refresh_interval: 5 * 1000, // milliseconds 57 | environments: [] 58 | } 59 | 60 | const mutations = { 61 | SET_CONFIG(state, config) { 62 | stateMerge(state, config) 63 | } 64 | } 65 | 66 | const actions = { 67 | updateConfig({commit}, config) { 68 | commit('SET_CONFIG', config) 69 | } 70 | } 71 | 72 | const getters = { 73 | getConfig: state => setting => { 74 | return state[setting] 75 | } 76 | } 77 | 78 | export default { 79 | state, 80 | mutations, 81 | actions, 82 | getters 83 | } 84 | -------------------------------------------------------------------------------- /src/store/modules/customers.store.ts: -------------------------------------------------------------------------------- 1 | import CustomersApi from '@/services/api/customer.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | isLoading: false, 7 | 8 | customers: [] 9 | } 10 | 11 | const mutations = { 12 | SET_LOADING(state) { 13 | state.isLoading = true 14 | }, 15 | SET_CUSTOMERS(state, customers) { 16 | state.isLoading = false 17 | state.customers = customers 18 | }, 19 | RESET_LOADING(state) { 20 | state.isLoading = false 21 | } 22 | } 23 | 24 | const actions = { 25 | getCustomers({commit}) { 26 | commit('SET_LOADING') 27 | return CustomersApi.getCustomers({}) 28 | .then(({customers}) => commit('SET_CUSTOMERS', customers)) 29 | .catch(() => commit('RESET_LOADING')) 30 | }, 31 | createCustomer({dispatch, commit}, customer) { 32 | return CustomersApi.createCustomer(customer).then(response => { 33 | dispatch('getCustomers') 34 | }) 35 | }, 36 | updateCustomer({dispatch, commit}, [customerId, update]) { 37 | return CustomersApi.updateCustomer(customerId, update).then(response => { 38 | dispatch('getCustomers') 39 | }) 40 | }, 41 | deleteCustomer({dispatch, commit}, customerId) { 42 | return CustomersApi.deleteCustomer(customerId).then(response => { 43 | dispatch('getCustomers') 44 | }) 45 | } 46 | } 47 | 48 | const getters = { 49 | customers: state => { 50 | return state.customers.map(c => c.customer) 51 | } 52 | } 53 | 54 | export default { 55 | namespaced, 56 | state, 57 | mutations, 58 | actions, 59 | getters 60 | } 61 | -------------------------------------------------------------------------------- /src/store/modules/groups.store.ts: -------------------------------------------------------------------------------- 1 | import GroupsApi from '@/services/api/group.service' 2 | import i18n from '@/plugins/i18n' 3 | 4 | const namespaced = true 5 | 6 | const state = { 7 | isLoading: false, 8 | 9 | groups: [], 10 | group: {}, 11 | users: [] 12 | } 13 | 14 | const mutations = { 15 | SET_LOADING(state) { 16 | state.isLoading = true 17 | }, 18 | SET_GROUPS(state, groups) { 19 | state.isLoading = false 20 | state.groups = groups 21 | }, 22 | SET_GROUP(state, group): any { 23 | state.group = group 24 | }, 25 | SET_GROUP_USERS(state, users) { 26 | state.isLoading = false 27 | state.users = users 28 | }, 29 | RESET_GROUP_USERS(state) { 30 | state.users = [] 31 | }, 32 | RESET_LOADING(state) { 33 | state.isLoading = false 34 | } 35 | } 36 | 37 | const actions = { 38 | getGroups({commit}) { 39 | commit('SET_LOADING') 40 | return GroupsApi.getGroups({}) 41 | .then(({groups}) => commit('SET_GROUPS', groups)) 42 | .catch(() => commit('RESET_LOADING')) 43 | }, 44 | getGroup({commit}, groupId) { 45 | return GroupsApi.getGroup(groupId).then(({group}) => { 46 | commit('SET_GROUP', group) 47 | }) 48 | }, 49 | getGroupUsers({commit}, groupId) { 50 | commit('SET_LOADING') 51 | return GroupsApi.getGroupUsers(groupId) 52 | .then(({users}) => commit('SET_GROUP_USERS', users)) 53 | .catch(() => commit('RESET_LOADING')) 54 | }, 55 | clearGroupUsers({commit}) { 56 | commit('RESET_GROUP_USERS') 57 | }, 58 | createGroup({dispatch, commit}, group) { 59 | return GroupsApi.createGroup(group).then(response => { 60 | dispatch('getGroups') 61 | }) 62 | }, 63 | updateGroup({dispatch, commit}, [groupId, update]) { 64 | return GroupsApi.updateGroup(groupId, update).then(response => { 65 | dispatch('getGroups') 66 | }) 67 | }, 68 | addUserToGroup({dispatch, commit}, [groupId, userId]) { 69 | return GroupsApi.addUserToGroup(groupId, userId) 70 | .then(response => { 71 | dispatch('getGroupUsers', groupId) 72 | }) 73 | .then(() => 74 | dispatch('notifications/success', i18n.t('UserAddedGroup'), { 75 | root: true 76 | }) 77 | ) 78 | }, 79 | removeUserFromGroup({dispatch, commit}, [groupId, userId]) { 80 | return GroupsApi.removeUserFromGroup(groupId, userId) 81 | .then(response => { 82 | dispatch('getGroupUsers', groupId) 83 | }) 84 | .then(() => 85 | dispatch('notifications/success', i18n.t('UserRemovedGroup'), { 86 | root: true 87 | }) 88 | ) 89 | }, 90 | deleteGroup({dispatch, commit}, groupId) { 91 | return GroupsApi.deleteGroup(groupId).then(response => { 92 | dispatch('getGroups') 93 | }) 94 | } 95 | } 96 | 97 | const getters = { 98 | // 99 | } 100 | 101 | export default { 102 | namespaced, 103 | state, 104 | mutations, 105 | actions, 106 | getters 107 | } 108 | -------------------------------------------------------------------------------- /src/store/modules/heartbeats.store.ts: -------------------------------------------------------------------------------- 1 | import HeartbeatsApi from '@/services/api/heartbeat.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | isLoading: false, 7 | 8 | heartbeats: [] 9 | } 10 | 11 | const mutations = { 12 | SET_LOADING(state) { 13 | state.isLoading = true 14 | }, 15 | SET_HEARTBEATS(state, heartbeats) { 16 | state.isLoading = false 17 | state.heartbeats = heartbeats 18 | }, 19 | RESET_LOADING(state) { 20 | state.isLoading = false 21 | } 22 | } 23 | 24 | const actions = { 25 | getHeartbeats({commit}) { 26 | commit('SET_LOADING') 27 | return HeartbeatsApi.getHeartbeats({}) 28 | .then(({heartbeats}) => commit('SET_HEARTBEATS', heartbeats)) 29 | .catch(() => commit('RESET_LOADING')) 30 | }, 31 | deleteHeartbeat({dispatch, commit}, heartbeatId) { 32 | return HeartbeatsApi.deleteHeartbeat(heartbeatId).then(response => { 33 | dispatch('getHeartbeats') 34 | }) 35 | } 36 | } 37 | 38 | const getters = { 39 | // 40 | } 41 | 42 | export default { 43 | namespaced, 44 | state, 45 | mutations, 46 | actions, 47 | getters 48 | } 49 | -------------------------------------------------------------------------------- /src/store/modules/keys.store.ts: -------------------------------------------------------------------------------- 1 | import KeysApi from '@/services/api/key.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | isLoading: false, 7 | 8 | keys: [] 9 | } 10 | 11 | const mutations = { 12 | SET_LOADING(state) { 13 | state.isLoading = true 14 | }, 15 | SET_USERS(state, users) { 16 | state.isLoading = false 17 | state.users = users 18 | }, 19 | SET_KEYS(state, keys) { 20 | state.isLoading = false 21 | state.keys = keys 22 | }, 23 | RESET_LOADING(state) { 24 | state.isLoading = false 25 | } 26 | } 27 | 28 | const actions = { 29 | getKeys({commit, dispatch}) { 30 | commit('SET_LOADING') 31 | return KeysApi.getKeys({}) 32 | .then(({keys}) => commit('SET_KEYS', keys)) 33 | .catch(() => commit('RESET_LOADING')) 34 | }, 35 | createKey({dispatch, commit}, key) { 36 | return KeysApi.createKey(key).then(response => { 37 | dispatch('getKeys') 38 | }) 39 | }, 40 | updateKey({dispatch, commit}, [key, update]) { 41 | return KeysApi.updateKey(key, update).then(response => { 42 | dispatch('getKeys') 43 | }) 44 | }, 45 | deleteKey({dispatch, commit}, key) { 46 | return KeysApi.deleteKey(key).then(response => { 47 | dispatch('getKeys') 48 | }) 49 | } 50 | } 51 | 52 | const getters = { 53 | // 54 | } 55 | 56 | export default { 57 | namespaced, 58 | state, 59 | mutations, 60 | actions, 61 | getters 62 | } 63 | -------------------------------------------------------------------------------- /src/store/modules/management.store.ts: -------------------------------------------------------------------------------- 1 | import ManagementApi from '@/services/api/management.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | manifest: null, 7 | 8 | healthcheck: null, 9 | 10 | application: null, 11 | metrics: [], 12 | time: null, 13 | uptime: null, 14 | version: null 15 | } 16 | 17 | const mutations = { 18 | SET_LOADING(state) { 19 | state.isLoading = true 20 | }, 21 | SET_MANIFEST(state, manifest) { 22 | state.manifest = manifest 23 | }, 24 | SET_HEALTHCHECK(state, healthcheck) { 25 | state.healthcheck = healthcheck 26 | }, 27 | SET_STATUS(state, status) { 28 | state.application = status.application 29 | state.metrics = status.metrics 30 | state.time = status.time 31 | state.uptime = status.uptime 32 | state.version = status.version 33 | } 34 | } 35 | 36 | const actions = { 37 | getManifest({commit, dispatch}) { 38 | return ManagementApi.manifest().then(manifest => commit('SET_MANIFEST', manifest)) 39 | }, 40 | getHealthcheck({commit, dispatch}) { 41 | return ManagementApi.healthcheck().then(healthcheck => commit('SET_HEALTHCHECK', healthcheck)) 42 | }, 43 | getStatus({commit, dispatch}) { 44 | return ManagementApi.status().then(status => commit('SET_STATUS', status)) 45 | } 46 | } 47 | 48 | const getters = { 49 | // 50 | } 51 | 52 | export default { 53 | namespaced, 54 | state, 55 | mutations, 56 | actions, 57 | getters 58 | } 59 | -------------------------------------------------------------------------------- /src/store/modules/notifications.store.ts: -------------------------------------------------------------------------------- 1 | const namespaced = true 2 | 3 | const state = { 4 | snackbars: [], 5 | banners: [] 6 | } 7 | 8 | // SNACKBAR 9 | // { 10 | // type: 'success', 'info', 'error' 11 | // text: '', 12 | // action: 'RETRY', 13 | // timeout: 6000 14 | // } 15 | 16 | // BANNER 17 | // { 18 | // type: success, info, warning or error 19 | // icon: null, // check_circle, info, priority_high, warning, 20 | // text: '' 21 | // } 22 | 23 | const mutations = { 24 | ADD_SNACKBAR(state, snackbar) { 25 | if (!state.snackbars.map(s => s.text).includes(snackbar.text)) { 26 | state.snackbars.push(snackbar) 27 | } 28 | }, 29 | REMOVE_SNACKBAR(state) { 30 | state.snackbars.shift() 31 | }, 32 | ADD_BANNER(state, banner) { 33 | if (!state.banners.map(b => b.text).includes(banner.text)) { 34 | state.banners.push(banner) 35 | } 36 | }, 37 | REMOVE_BANNER(state) { 38 | state.banners.shift() 39 | } 40 | } 41 | 42 | const actions = { 43 | showSnackbar({commit}, snackbar) { 44 | commit('ADD_SNACKBAR', snackbar) 45 | }, 46 | closeSnackbar({commit}) { 47 | commit('REMOVE_SNACKBAR') 48 | }, 49 | showBanner({commit}, banner) { 50 | commit('ADD_BANNER', banner) 51 | }, 52 | closeBanner({commit}) { 53 | commit('REMOVE_BANNER') 54 | }, 55 | 56 | success({commit}, message) { 57 | commit('ADD_SNACKBAR', { 58 | type: 'success', 59 | text: message, 60 | action: 'OK', 61 | timeout: 3000 62 | }) 63 | }, 64 | 65 | error({commit}, error) { 66 | // HTTP error with status, code, message and errors. 67 | if (error.hasOwnProperty('code')) { 68 | commit('ADD_SNACKBAR', { 69 | type: error.status, 70 | text: `${error.message} (${error.code})`, 71 | action: 'CLOSE', 72 | timeout: 5000 73 | }) 74 | } else { 75 | commit('ADD_SNACKBAR', { 76 | type: 'error', 77 | text: `${error.name}: ${error.message}`, 78 | action: 'CLOSE', 79 | timeout: 5000 80 | }) 81 | } 82 | } 83 | } 84 | 85 | const getters = { 86 | hasSnackbar: state => { 87 | return state.snackbars.length > 0 88 | }, 89 | hasBanners: state => { 90 | return state.banners.length > 0 91 | } 92 | } 93 | 94 | export default { 95 | namespaced, 96 | state, 97 | mutations, 98 | actions, 99 | getters 100 | } 101 | -------------------------------------------------------------------------------- /src/store/modules/perms.store.ts: -------------------------------------------------------------------------------- 1 | import PermsApi from '@/services/api/perms.service' 2 | 3 | const namespaced = true 4 | 5 | const state = { 6 | isLoading: false, 7 | 8 | permissions: [], 9 | scopes: [] 10 | } 11 | 12 | const mutations = { 13 | SET_LOADING(state) { 14 | state.isLoading = true 15 | }, 16 | SET_PERMS(state, permissions) { 17 | state.isLoading = false 18 | state.permissions = permissions 19 | }, 20 | SET_SCOPES(state, scopes) { 21 | state.isLoading = false 22 | state.scopes = scopes 23 | }, 24 | RESET_LOADING(state) { 25 | state.isLoading = false 26 | } 27 | } 28 | 29 | const actions = { 30 | getPerms({commit}) { 31 | commit('SET_LOADING') 32 | return PermsApi.getPerms({}) 33 | .then(({permissions}) => commit('SET_PERMS', permissions)) 34 | .catch(() => commit('RESET_LOADING')) 35 | }, 36 | createPerm({dispatch, commit}, perm) { 37 | return PermsApi.createPerm(perm).then(response => { 38 | dispatch('getPerms') 39 | }) 40 | }, 41 | updatePerm({dispatch, commit}, [permId, update]) { 42 | return PermsApi.updatePerm(permId, update).then(response => { 43 | dispatch('getPerms') 44 | }) 45 | }, 46 | deletePerm({dispatch, commit}, permId) { 47 | return PermsApi.deletePerm(permId).then(response => { 48 | dispatch('getPerms') 49 | }) 50 | }, 51 | 52 | getScopes({commit}) { 53 | commit('SET_LOADING') 54 | return PermsApi.getScopes().then(({scopes}) => commit('SET_SCOPES', scopes)) 55 | } 56 | } 57 | 58 | const getters = { 59 | roles: state => { 60 | return state.permissions.map(p => p.match) 61 | } 62 | } 63 | 64 | export default { 65 | namespaced, 66 | state, 67 | mutations, 68 | actions, 69 | getters 70 | } 71 | -------------------------------------------------------------------------------- /src/store/modules/preferences.store.ts: -------------------------------------------------------------------------------- 1 | import UsersApi from '@/services/api/user.service' 2 | import stateMerge from 'vue-object-merge' 3 | import i18n from '@/plugins/i18n' 4 | 5 | const getDefaults = () => { 6 | return { 7 | isDark: false, 8 | isMute: true, 9 | languagePref: i18n.locale, 10 | audioURL: './audio/alert_high-intensity.ogg', 11 | dates: { 12 | longDate: null, 13 | mediumDate: null, 14 | shortTime: null 15 | }, 16 | timezone: 'local', // 'local' or 'utc' 17 | displayDensity: null, // 'comfortable' or 'compact' 18 | showAllowedEnvs: false, 19 | showNotesIcon: false, 20 | font: { 21 | 'font-family': null, 22 | 'font-size': null, 23 | 'font-weight': null 24 | }, 25 | rowsPerPage: 20, 26 | valueWidth: 50, // px 27 | textWidth: 400, // px 28 | refreshInterval: 5 * 1000, // milliseconds 29 | ackTimeout: null, 30 | shelveTimeout: null, 31 | blackoutStartNow: true, 32 | blackoutPeriod: null, 33 | queries: [] 34 | } 35 | } 36 | 37 | const state = getDefaults() 38 | 39 | const mutations = { 40 | SET_PREFS(state, prefs) { 41 | stateMerge(state, prefs) 42 | }, 43 | RESET_PREFS(state) { 44 | let q = state.queries 45 | Object.assign(state, getDefaults()) 46 | stateMerge(state, {queries: q}) 47 | }, 48 | SET_QUERIES(state, queries) { 49 | stateMerge(state, {queries: queries || []}) 50 | }, 51 | RESET_QUERIES(state) { 52 | Object.assign(state, {queries: []}) 53 | } 54 | } 55 | 56 | const actions = { 57 | getUserPrefs({dispatch, commit}) { 58 | return UsersApi.getMeAttributes() 59 | .then(({attributes}) => { 60 | commit('SET_PREFS', attributes.prefs) 61 | }) 62 | .catch(error => 63 | dispatch('notifications/error', Error('' + i18n.t('SettingsError')), { 64 | root: true 65 | }) 66 | ) 67 | }, 68 | toggle({dispatch, commit}, [s, v]) { 69 | return UsersApi.updateMeAttributes({prefs: {[s]: v}}) 70 | .then(response => dispatch('getUserPrefs')) 71 | .then(() => 72 | dispatch('notifications/success', i18n.t('SettingsSaved'), { 73 | root: true 74 | }) 75 | ) 76 | }, 77 | setUserPrefs({dispatch, commit}, prefs) { 78 | return UsersApi.updateMeAttributes({prefs: prefs}) 79 | .then(response => dispatch('getUserPrefs')) 80 | .then(() => 81 | dispatch('notifications/success', i18n.t('SettingsSaved'), { 82 | root: true 83 | }) 84 | ) 85 | }, 86 | resetUserPrefs({dispatch, commit}) { 87 | return UsersApi.updateMeAttributes({prefs: null}) 88 | .then(response => commit('RESET_PREFS')) 89 | .then(() => 90 | dispatch('notifications/success', i18n.t('SettingsReset'), { 91 | root: true 92 | }) 93 | ) 94 | }, 95 | clearUserPrefs({commit}) { 96 | commit('RESET_PREFS') 97 | }, 98 | getUserQueries({dispatch, commit}) { 99 | return UsersApi.getMeAttributes() 100 | .then(({attributes}) => { 101 | commit('SET_QUERIES', attributes.queries) 102 | }) 103 | .catch(error => 104 | dispatch('notifications/error', Error('' + i18n.t('SettingsError')), { 105 | root: true 106 | }) 107 | ) 108 | }, 109 | addUserQuery({dispatch, state}, query) { 110 | let qlist = state.queries.filter(q => q.text != query.text).concat([query]) 111 | return UsersApi.updateMeAttributes({queries: qlist}) 112 | .then(response => dispatch('getUserQueries')) 113 | .then(() => 114 | dispatch('notifications/success', i18n.t('SettingsSaved'), { 115 | root: true 116 | }) 117 | ) 118 | }, 119 | removeUserQuery({dispatch, state}, query) { 120 | let qlist = state.queries.filter(q => q.text != query.text) 121 | return UsersApi.updateMeAttributes({queries: qlist}) 122 | .then(response => dispatch('getUserQueries')) 123 | .then(() => 124 | dispatch('notifications/success', i18n.t('SettingsSaved'), { 125 | root: true 126 | }) 127 | ) 128 | }, 129 | resetUserQueries({dispatch, commit}) { 130 | return UsersApi.updateMeAttributes({queries: null}) 131 | .then(response => commit('RESET_QUERIES')) 132 | .then(() => 133 | dispatch('notifications/success', i18n.t('SettingsReset'), { 134 | root: true 135 | }) 136 | ) 137 | } 138 | } 139 | 140 | const getters = { 141 | getPreference: state => pref => { 142 | return state[pref] 143 | }, 144 | getUserQueries: state => { 145 | return state.queries ? state.queries : [] 146 | } 147 | } 148 | 149 | export default { 150 | state, 151 | mutations, 152 | actions, 153 | getters 154 | } 155 | -------------------------------------------------------------------------------- /src/store/modules/reports.store.ts: -------------------------------------------------------------------------------- 1 | import AlertsApi from '@/services/api/alert.service' 2 | 3 | import moment from 'moment' 4 | 5 | const namespaced = true 6 | 7 | const state = { 8 | offenders: [], 9 | flapping: [], 10 | standing: [], 11 | 12 | filter: { 13 | environment: null, 14 | severity: null, 15 | status: ['open', 'ack'], 16 | customer: null, 17 | service: null, 18 | group: null, 19 | dateRange: [null, null] 20 | }, 21 | 22 | pagination: { 23 | page: 1, 24 | rowsPerPage: 10 25 | } 26 | } 27 | 28 | const mutations = { 29 | SET_TOP_OFFENDERS(state, top10): any { 30 | state.offenders = top10 31 | }, 32 | SET_TOP_FLAPPING(state, top10): any { 33 | state.flapping = top10 34 | }, 35 | SET_TOP_STANDING(state, top10): any { 36 | state.standing = top10 37 | }, 38 | SET_FILTER(state, filter): any { 39 | state.filter = Object.assign({}, state.filter, filter) 40 | }, 41 | SET_PAGE_SIZE(state, rowsPerPage) { 42 | state.pagination.rowsPerPage = rowsPerPage 43 | } 44 | } 45 | 46 | function getParams(state) { 47 | // get "lucene" query params (?q=) 48 | let params = new URLSearchParams(state.query) 49 | 50 | // append filter params to query params 51 | state.filter.environment && params.append('environment', state.filter.environment) 52 | state.filter.severity && state.filter.severity.map(sv => params.append('severity', sv)) 53 | state.filter.status && state.filter.status.map(st => params.append('status', st)) 54 | state.filter.customer && state.filter.customer.map(c => params.append('customer', c)) 55 | state.filter.service && state.filter.service.map(s => params.append('service', s)) 56 | state.filter.group && state.filter.group.map(g => params.append('group', g)) 57 | 58 | // add server-side paging 59 | params.append('page', state.pagination.page) 60 | params.append('page-size', state.pagination.rowsPerPage) 61 | 62 | // apply any date/time filters 63 | if (state.filter.dateRange[0] > 0) { 64 | params.append( 65 | 'from-date', 66 | moment.unix(state.filter.dateRange[0]).toISOString() // epoch seconds 67 | ) 68 | } else if (state.filter.dateRange[0] < 0) { 69 | params.append( 70 | 'from-date', 71 | moment().utc().add(state.filter.dateRange[0], 'seconds').toISOString() // seconds offset 72 | ) 73 | } 74 | if (state.filter.dateRange[1] > 0) { 75 | params.append( 76 | 'to-date', 77 | moment.unix(state.filter.dateRange[1]).toISOString() // epoch seconds 78 | ) 79 | } else if (state.filter.dateRange[1] < 0) { 80 | params.append( 81 | 'to-date', 82 | moment().utc().add(state.filter.dateRange[1], 'seconds').toISOString() // seconds offset 83 | ) 84 | } 85 | return params 86 | } 87 | 88 | const actions = { 89 | getTopOffenders({commit, state}) { 90 | let params = getParams(state) 91 | return AlertsApi.getTop10Count(params).then(({top10}) => commit('SET_TOP_OFFENDERS', top10)) 92 | }, 93 | getTopFlapping({commit, state}) { 94 | let params = getParams(state) 95 | return AlertsApi.getTop10Flapping(params).then(({top10}) => commit('SET_TOP_FLAPPING', top10)) 96 | }, 97 | getTopStanding({commit, state}) { 98 | let params = getParams(state) 99 | return AlertsApi.getTop10Standing(params).then(({top10}) => commit('SET_TOP_STANDING', top10)) 100 | }, 101 | 102 | setFilter({commit}, filter) { 103 | commit('SET_FILTER', filter) 104 | }, 105 | resetFilter({commit, rootState}) { 106 | commit('SET_FILTER', rootState.config.filter) 107 | }, 108 | setPageSize({commit}, rowsPerPage) { 109 | commit('SET_PAGE_SIZE', rowsPerPage) 110 | } 111 | } 112 | 113 | const getters = {} 114 | 115 | export default { 116 | namespaced, 117 | state, 118 | mutations, 119 | actions, 120 | getters 121 | } 122 | -------------------------------------------------------------------------------- /src/store/modules/users.store.ts: -------------------------------------------------------------------------------- 1 | import UsersApi from '@/services/api/user.service' 2 | import i18n from '@/plugins/i18n' 3 | 4 | const namespaced = true 5 | 6 | const state = { 7 | isLoading: false, 8 | 9 | domains: [], 10 | users: [], 11 | groups: [] 12 | } 13 | 14 | const mutations = { 15 | SET_LOADING(state) { 16 | state.isLoading = true 17 | }, 18 | SET_USERS(state, users) { 19 | state.isLoading = false 20 | state.users = users 21 | }, 22 | SET_USER_GROUPS(state, groups) { 23 | state.groups = groups 24 | }, 25 | RESET_USER_GROUPS(state) { 26 | state.groups = [] 27 | }, 28 | RESET_LOADING(state) { 29 | state.isLoading = false 30 | } 31 | } 32 | 33 | const actions = { 34 | getUsers({commit}) { 35 | commit('SET_LOADING') 36 | return UsersApi.getUsers({}) 37 | .then(({users}) => commit('SET_USERS', users)) 38 | .catch(() => commit('RESET_LOADING')) 39 | }, 40 | createUser({dispatch, commit}, user) { 41 | return UsersApi.createUser(user).then(response => { 42 | dispatch('getUsers') 43 | }) 44 | }, 45 | updateUser({dispatch, commit}, [userId, update]) { 46 | return UsersApi.updateUser(userId, update).then(response => { 47 | dispatch('getUsers') 48 | }) 49 | }, 50 | setUserStatus({dispatch, commit}, [userId, status]) { 51 | return UsersApi.updateUser(userId, {status: status}) 52 | .then(response => { 53 | dispatch('getUsers') 54 | }) 55 | .then(() => 56 | dispatch('notifications/success', i18n.t('UserStatusSaved'), { 57 | root: true 58 | }) 59 | ) 60 | }, 61 | setEmailVerified({dispatch, commit}, [userId, emailVerified]) { 62 | return UsersApi.updateUser(userId, {email_verified: emailVerified}) 63 | .then(response => { 64 | dispatch('getUsers') 65 | }) 66 | .then(() => dispatch('notifications/success', i18n.t('EmailSaved'), {root: true})) 67 | }, 68 | deleteUser({dispatch, commit}, userId) { 69 | return UsersApi.deleteUser(userId).then(response => { 70 | dispatch('getUsers') 71 | }) 72 | }, 73 | getUserGroups({dispatch, commit}, userId) { 74 | return UsersApi.getGroups(userId).then(({groups}) => commit('SET_USER_GROUPS', groups)) 75 | }, 76 | resetUserGroups({commit}) { 77 | commit('RESET_USER_GROUPS') 78 | } 79 | } 80 | 81 | const getters = { 82 | // 83 | } 84 | 85 | export default { 86 | namespaced, 87 | state, 88 | mutations, 89 | actions, 90 | getters 91 | } 92 | -------------------------------------------------------------------------------- /src/stylus/main.styl: -------------------------------------------------------------------------------- 1 | 2 | @import '~vuetify/src/stylus/main' 3 | @import '~vuetify/src/stylus/app' 4 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/views/Alert.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/views/ApiKeys.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Blackouts.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Confirm.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Customers.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Forgot.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Groups.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Heartbeats.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Logout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Perms.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Reports.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 78 | -------------------------------------------------------------------------------- /src/views/Reset.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/Signup.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/views/Users.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "https_only": true, 3 | "root": "dist", 4 | "clean_urls": true, 5 | "routes": { 6 | "/**": "index.html" 7 | }, 8 | "headers": { 9 | "/": { 10 | "Cache-Control": "no-cache, no-store" 11 | }, 12 | "/js/*": { 13 | "Cache-Control": "public, max-age=31536000" 14 | }, 15 | "/css/*": { 16 | "Cache-Control": "public, max-age=31536000" 17 | }, 18 | "/fonts/*": { 19 | "Cache-Control": "public, max-age=31536000" 20 | }, 21 | "/audio/*": { 22 | "Cache-Control": "public, max-age=31536000" 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | mocha: true, 5 | 'cypress/globals': true 6 | }, 7 | rules: { 8 | strict: 'off' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | // if you need a custom webpack configuration you can uncomment the following import 4 | // and then use the `file:preprocessor` event 5 | // as explained in the cypress docs 6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 7 | 8 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 9 | // const webpack = require('@cypress/webpack-preprocessor') 10 | 11 | module.exports = (on, config) => { 12 | // on('file:preprocessor', webpack({ 13 | // webpackOptions: require('@vue/cli-service/webpack.config'), 14 | // watchOptions: {} 15 | // })) 16 | 17 | return Object.assign({}, config, { 18 | fixturesFolder: 'tests/e2e/fixtures', 19 | integrationFolder: 'tests/e2e/specs', 20 | screenshotsFolder: 'tests/e2e/screenshots', 21 | videosFolder: 'tests/e2e/videos', 22 | supportFile: 'tests/e2e/support/index.js' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'Welcome to Vuetify') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/ApiKeyList.spec.ts: -------------------------------------------------------------------------------- 1 | import {shallowMount} from '@vue/test-utils' 2 | import ApiKeyList from '@/components/ApiKeyList.vue' 3 | import Vue from 'vue' 4 | import Vuex, {Store} from 'vuex' 5 | import Vuetify from 'vuetify' 6 | import i18n from '@/plugins/i18n' 7 | 8 | Vue.config.silent = true 9 | Vue.use(Vuetify) 10 | Vue.use(Vuex) 11 | 12 | describe('ApiKeyList', () => { 13 | let store: Store 14 | 15 | beforeEach(() => { 16 | store = new Vuex.Store({ 17 | modules: { 18 | keys: { 19 | namespaced: true, 20 | state: { 21 | keys: [ 22 | { 23 | count: 0, 24 | customer: 'Google', 25 | expireTime: '2020-01-11T17:37:48.569Z', 26 | href: 'http://api.local.alerta.io:8080/key/vj50CYx04fpviPlyhQiz-l_XVOPZsWzSR9PIzRHH', 27 | id: '4429e9bf-f7b7-4a9d-b1bd-7252ee84635d', 28 | key: 'vj50CYx04fpviPlyhQiz-l_XVOPZsWzSR9PIzRHH', 29 | lastUsedTime: null, 30 | scopes: ['write', 'read'], 31 | text: '', 32 | type: 'read-write', 33 | user: 'nfsatterly@gmail.com' 34 | } 35 | ], 36 | 37 | pagination: { 38 | page: 1, 39 | rowsPerPage: 20, 40 | totalItems: 1, 41 | sortBy: 'lastUsedTime', 42 | descending: true, 43 | rowsPerPageItems: [10, 20, 50, 100, 200] 44 | } 45 | }, 46 | actions: { 47 | getKeys() {} 48 | }, 49 | getters: { 50 | pagination: state => { 51 | return state.pagination 52 | } 53 | } 54 | }, 55 | perms: { 56 | namespaced: true, 57 | state: { 58 | permissions: [], 59 | scopes: [] 60 | }, 61 | actions: { 62 | getScopes() { 63 | return ['read', 'write'] // default user scopes 64 | } 65 | } 66 | }, 67 | users: { 68 | namespaced: true, 69 | state: { 70 | domains: [], 71 | users: [], 72 | groups: [] 73 | }, 74 | actions: { 75 | getUsers() {} 76 | } 77 | }, 78 | customers: { 79 | namespaced: true, 80 | state: { 81 | customers: [] 82 | }, 83 | actions: { 84 | getCustomers() {} 85 | } 86 | }, 87 | auth: { 88 | namespaced: true, 89 | state: { 90 | isAuthenticated: true 91 | }, 92 | getters: { 93 | scopes() { 94 | return [] 95 | } 96 | } 97 | } 98 | } 99 | }) 100 | }) 101 | 102 | it('renders props.msg when passed', () => { 103 | const msg = 'Sorry, nothing to display here :(' 104 | const wrapper = shallowMount(ApiKeyList, { 105 | propsData: {msg}, 106 | store, 107 | i18n, 108 | mocks: { 109 | $config: () => true 110 | } 111 | }) 112 | expect(wrapper.text()).toMatch(msg) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /tests/unit/components/common/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import utils from '@/common/utils' 2 | 3 | describe('Utils', () => { 4 | let allScopes = [ 5 | 'read', 6 | 'write', 7 | 'admin', 8 | 'read:alerts', 9 | 'write:alerts', 10 | 'admin:alerts', 11 | 'read:blackouts', 12 | 'write:blackouts', 13 | 'admin:blackouts', 14 | 'read:heartbeats', 15 | 'write:heartbeats', 16 | 'admin:heartbeats', 17 | 'write:users', 18 | 'admin:users', 19 | 'read:perms', 20 | 'admin:perms', 21 | 'read:customers', 22 | 'admin:customers', 23 | 'read:keys', 24 | 'write:keys', 25 | 'admin:keys', 26 | 'write:webhooks', 27 | 'read:oembed', 28 | 'read:management', 29 | 'admin:management', 30 | 'read:userinfo' 31 | ] 32 | 33 | it('derives full scopes from assigned scopes', () => { 34 | let result = utils.getAllowedScopes(['admin:perms', 'read', 'write:keys'], allScopes) 35 | let expected = [ 36 | 'admin:perms', 37 | 'read:perms', 38 | 'read', 39 | 'read:alerts', 40 | 'read:blackouts', 41 | 'read:heartbeats', 42 | 'read:customers', 43 | 'read:oembed', 44 | 'read:management', 45 | 'read:userinfo', 46 | 'write:keys', 47 | 'read:keys' 48 | ] 49 | expect(result.sort()).toEqual(expected.sort()) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "noImplicitAny": false, 15 | "types": [ 16 | "webpack-env", 17 | "jest" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | process.env.VUE_APP_VERSION = require('./package.json').version 2 | 3 | module.exports = { 4 | publicPath: process.env.BASE_URL, 5 | chainWebpack: config => { 6 | config.module 7 | .rule('fonts') 8 | .use('url-loader') 9 | .loader('url-loader') 10 | .options({ 11 | limit: 4096, 12 | name: 'fonts/[name].[ext]' 13 | }) 14 | }, 15 | devServer: { 16 | disableHostCheck: true 17 | } 18 | } 19 | --------------------------------------------------------------------------------