Merge branch 'dev' into contributors

pull/725/head
Henri Cook 2020-09-29 18:08:48 +01:00 committed by GitHub
commit bc2228e526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
247 changed files with 14933 additions and 6926 deletions

View File

@ -2,6 +2,7 @@
.github
.idea
logs
certs
*.log
build
assets

3
.github/FUNDING.yml vendored
View File

@ -1,3 +1,4 @@
github: hunterlong
patreon: statping
custom: ['https://www.nfoservers.com/donate.pl?force_recipient=1&recipient=info%40socialeck.com', 'https://opencollective.com/statping', 'https://www.buymeacoffee.com/hunterlong']
open_collective: statping
custom: ['https://www.nfoservers.com/donate.pl?force_recipient=1&recipient=info%40socialeck.com', 'https://www.buymeacoffee.com/hunterlong']

View File

@ -1,4 +1,4 @@
name: Dev Release
name: Development Build
on:
push:
branches:
@ -7,40 +7,46 @@ on:
- '**.md'
jobs:
compile:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
node-version: 12.18.2
- name: Add GOBIN to PATH
- name: Set Version
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
run: npm install -g yarn sass cross-env mjml
- name: Download Frontend Dependencies
if: steps.nodecache.outputs.cache-hit != 'true'
working-directory: ./frontend
run: yarn
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Build Frontend Statping
run: make clean compile
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: make clean generate compile
- name: Upload Compiled Frontend (rice-box.go)
uses: actions/upload-artifact@v1
@ -48,10 +54,145 @@ jobs:
name: static-rice-box
path: ./source
test:
needs: compile
runs-on: ubuntu-latest
- name: Configure AWS credentials for Asset uploads
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Upload Static Assets to S3
run: |
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/commit/${{ github.sha }}/
rm -rf source.tar.gz
build:
needs: frontend
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux]
arch: [386, amd64, arm-7, arm-6, arm64]
include:
- platform: darwin
arch: amd64
- platform: windows
arch: amd64
steps:
- uses: actions/checkout@v2
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Configure AWS credentials for Asset uploads
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Add GOBIN to PATH
run: |
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Set Linux Build Flags
if: matrix.platform == 'linux'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,linux,sqlite_omit_load_extension'
shell: bash
- name: Set Darwin Build Flags
if: matrix.platform == 'darwin'
run: echo ::set-env name=XGO_TAGS::'netgo,osusergo,darwin,sqlite_omit_load_extension'
shell: bash
- name: Set Windows Build Flags
if: matrix.platform == 'windows'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,sqlite_omit_load_extension'
shell: bash
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
uses: crazy-max/ghaction-xgo@v1
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
with:
xgo_version: latest
go_version: 1.15.x
dest: build
prefix: statping
targets: ${{ matrix.platform }}/${{ matrix.arch }}
v: false
x: false
pkg: cmd
buildmode: pie
tags: ${{ env.XGO_TAGS }}
ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }}
- name: Compress Linux Builds
if: matrix.platform == 'linux'
run: |
cd build
mv statping-linux-${{ matrix.arch }} statping
chmod +x statping
tar -czvf statping-linux-${{ matrix.arch }}.tar.gz statping
rm -rf statping
echo ::set-env name=compressed::statping-linux-${{ matrix.arch }}.tar.gz
- name: Compress Windows Builds
if: matrix.platform == 'windows'
run: |
cd build
mv statping-windows-4.0-${{ matrix.arch }}.exe statping.exe
chmod +x statping.exe
zip statping-windows-${{ matrix.arch }}.zip statping.exe
rm -rf statping.exe
echo ::set-env name=compressed::statping-windows-${{ matrix.arch }}.zip
- name: Compress Darwin Builds
if: matrix.platform == 'darwin'
run: |
cd build
mv statping-darwin-10.6-${{ matrix.arch }} statping
chmod +x statping
tar -czvf statping-darwin-${{ matrix.arch }}.tar.gz statping
rm -rf statping
echo ::set-env name=compressed::statping-darwin-${{ matrix.arch }}.tar.gz
- name: Upload Compiled Statping Binary
uses: actions/upload-artifact@v1
with:
name: statping-${{ matrix.platform }}-${{ matrix.arch }}
path: ./build
- name: Upload Releases
id: upload-assets
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: dev-v${{ env.VERSION }}
draft: true
prerelease: true
files: build/${{ env.compressed }}
- name: Upload Compiled Binaries to S3
run: |
aws s3 cp build/${{ env.compressed }} s3://assets.statping.com/commit/${{ github.sha }}/
test:
needs: frontend
runs-on: ubuntu-latest
services:
postgres:
image: postgres:10.8
@ -73,13 +214,13 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
node-version: 12.18.2
- name: Install Global Dependencies
run: npm install -g yarn sass newman cross-env wait-on @sentry/cli
@ -87,7 +228,7 @@ jobs:
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -100,6 +241,7 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build certs
chmod +x statping
@ -111,11 +253,13 @@ jobs:
gotestsum --no-summary=skipped --format dots -- -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
DB_CONN: sqlite3
STATPING_DIR: ${{ github.workspace }}
API_SECRET: demopassword123
DISABLE_LOGS: false
ALLOW_REPORTS: true
SAMPLE_DATA: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
@ -136,6 +280,12 @@ jobs:
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
TWILIO_TO: ${{ secrets.TWILIO_TO }}
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
SNS_TOKEN: ${{ secrets.SNS_TOKEN }}
SNS_SECRET: ${{ secrets.SNS_SECRET }}
SNS_REGION: ${{ secrets.SNS_REGION }}
SNS_TOPIC: ${{ secrets.SNS_TOPIC }}
- name: Coveralls Testing Coverage
run: |
@ -145,18 +295,18 @@ jobs:
COVERALLS: ${{ secrets.COVERALLS }}
test-postman-sqlite:
needs: compile
needs: frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -169,16 +319,15 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman SQLite Tests
uses: matt-ball/newman-action@master
with:
@ -189,7 +338,7 @@ jobs:
delayRequest: 600
test-postman-mysql:
needs: compile
needs: frontend
runs-on: ubuntu-latest
services:
@ -206,12 +355,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -224,16 +373,17 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman MySQL Tests
uses: matt-ball/newman-action@master
with:
@ -244,7 +394,7 @@ jobs:
delayRequest: 600
test-postman-postgres:
needs: compile
needs: frontend
runs-on: ubuntu-latest
services:
@ -262,12 +412,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -280,16 +430,15 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman Postgres Tests
uses: matt-ball/newman-action@master
with:
@ -300,7 +449,7 @@ jobs:
delayRequest: 600
docker-release:
needs: [test, test-postman-sqlite, test-postman-mysql, test-postman-postgres]
needs: [test, build, test-postman-sqlite, test-postman-mysql, test-postman-postgres]
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
@ -310,25 +459,47 @@ jobs:
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Base Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.base
tags: "base"
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v3
- name: Dev Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
- name: Docker Login
env:
VERSION: ${{ env.VERSION }}
ARCH: amd64
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Cache Docker layers
uses: actions/cache@v2
id: buildx-docker-master
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
tags: "dev"
buildargs: VERSION,ARCH
path: /tmp/.buildx-cache
key: buildx-docker-master
restore-keys: |
buildx-docker-master
- name: Docker Build :base
run: make buildx-base
- name: Docker Build :dev
run: make buildx-dev
sentry-release:
needs: [test, test-postman-sqlite, test-postman-postgres, test-postman-mysql]
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setup Sentry CLI
uses: mathrix-education/setup-sentry-cli@master
with:
version: latest
url: ${{ secrets.SENTRY_URL }}
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
organization: statping
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Sentry Release
run: make sentry-release

View File

@ -1,4 +1,4 @@
name: Master Release
name: Master Build
on:
push:
branches:
@ -7,40 +7,46 @@ on:
- '**.md'
jobs:
compile:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
node-version: 12.18.2
- name: Add GOBIN to PATH
- name: Set Version
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
run: npm install -g yarn sass cross-env mjml
- name: Download Frontend Dependencies
if: steps.nodecache.outputs.cache-hit != 'true'
working-directory: ./frontend
run: yarn
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Build Frontend Statping
run: make clean compile
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: make clean generate compile
- name: Upload Compiled Frontend (rice-box.go)
uses: actions/upload-artifact@v1
@ -48,10 +54,145 @@ jobs:
name: static-rice-box
path: ./source
test:
needs: compile
runs-on: ubuntu-latest
- name: Configure AWS credentials for Asset uploads
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Upload Static Assets to S3
run: |
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/commit/${{ github.sha }}/
rm -rf source.tar.gz
build:
needs: frontend
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux]
arch: [386, amd64, arm-7, arm-6, arm64]
include:
- platform: darwin
arch: amd64
- platform: windows
arch: amd64
steps:
- uses: actions/checkout@v2
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Configure AWS credentials for Asset uploads
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Add GOBIN to PATH
run: |
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Set Linux Build Flags
if: matrix.platform == 'linux'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,linux,sqlite_omit_load_extension'
shell: bash
- name: Set Darwin Build Flags
if: matrix.platform == 'darwin'
run: echo ::set-env name=XGO_TAGS::'netgo,osusergo,darwin,sqlite_omit_load_extension'
shell: bash
- name: Set Windows Build Flags
if: matrix.platform == 'windows'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,sqlite_omit_load_extension'
shell: bash
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
uses: crazy-max/ghaction-xgo@v1
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
with:
xgo_version: latest
go_version: 1.15.x
dest: build
prefix: statping
targets: ${{ matrix.platform }}/${{ matrix.arch }}
v: false
x: false
pkg: cmd
buildmode: pie
tags: ${{ env.XGO_TAGS }}
ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }}
- name: Compress Linux Builds
if: matrix.platform == 'linux'
run: |
cd build
mv statping-linux-${{ matrix.arch }} statping
chmod +x statping
tar -czvf statping-linux-${{ matrix.arch }}.tar.gz statping
rm -rf statping
echo ::set-env name=compressed::statping-linux-${{ matrix.arch }}.tar.gz
- name: Compress Windows Builds
if: matrix.platform == 'windows'
run: |
cd build
mv statping-windows-4.0-${{ matrix.arch }}.exe statping.exe
chmod +x statping.exe
zip statping-windows-${{ matrix.arch }}.zip statping.exe
rm -rf statping.exe
echo ::set-env name=compressed::statping-windows-${{ matrix.arch }}.zip
- name: Compress Darwin Builds
if: matrix.platform == 'darwin'
run: |
cd build
mv statping-darwin-10.6-${{ matrix.arch }} statping
chmod +x statping
tar -czvf statping-darwin-${{ matrix.arch }}.tar.gz statping
rm -rf statping
echo ::set-env name=compressed::statping-darwin-${{ matrix.arch }}.tar.gz
- name: Upload Compiled Statping Binary
uses: actions/upload-artifact@v1
with:
name: statping-${{ matrix.platform }}-${{ matrix.arch }}
path: ./build
- name: Upload Releases
id: upload-assets
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: v${{ env.VERSION }}
draft: true
prerelease: true
files: build/${{ env.compressed }}
- name: Upload Compiled Binaries to S3
run: |
aws s3 cp build/${{ env.compressed }} s3://assets.statping.com/commit/${{ github.sha }}/
test:
needs: frontend
runs-on: ubuntu-latest
services:
postgres:
image: postgres:10.8
@ -73,13 +214,13 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
node-version: 12.18.2
- name: Install Global Dependencies
run: npm install -g yarn sass newman cross-env wait-on @sentry/cli
@ -87,7 +228,7 @@ jobs:
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -100,6 +241,7 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build certs
chmod +x statping
@ -111,11 +253,13 @@ jobs:
gotestsum --no-summary=skipped --format dots -- -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
DB_CONN: sqlite3
STATPING_DIR: ${{ github.workspace }}
API_SECRET: demopassword123
DISABLE_LOGS: false
ALLOW_REPORTS: true
SAMPLE_DATA: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
@ -136,6 +280,12 @@ jobs:
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
TWILIO_TO: ${{ secrets.TWILIO_TO }}
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
SNS_TOKEN: ${{ secrets.SNS_TOKEN }}
SNS_SECRET: ${{ secrets.SNS_SECRET }}
SNS_REGION: ${{ secrets.SNS_REGION }}
SNS_TOPIC: ${{ secrets.SNS_TOPIC }}
- name: Coveralls Testing Coverage
run: |
@ -145,18 +295,18 @@ jobs:
COVERALLS: ${{ secrets.COVERALLS }}
test-postman-sqlite:
needs: compile
needs: frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -169,16 +319,15 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman SQLite Tests
uses: matt-ball/newman-action@master
with:
@ -189,7 +338,7 @@ jobs:
delayRequest: 600
test-postman-mysql:
needs: compile
needs: frontend
runs-on: ubuntu-latest
services:
@ -206,12 +355,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -224,16 +373,17 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman MySQL Tests
uses: matt-ball/newman-action@master
with:
@ -244,7 +394,7 @@ jobs:
delayRequest: 600
test-postman-postgres:
needs: compile
needs: frontend
runs-on: ubuntu-latest
services:
@ -262,12 +412,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -280,16 +430,15 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8585 > /dev/null &
sleep 5
- name: Postman Postgres Tests
uses: matt-ball/newman-action@master
with:
@ -299,110 +448,8 @@ jobs:
timeoutRequest: 30000
delayRequest: 600
build-binaries:
needs: compile
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/checkout@v2
- name: Install cross compiling libraries
run: |
sudo apt-get update
sudo apt-get install -y automake autogen build-essential ca-certificates libsqlite3-dev \
gcc-5-arm-linux-gnueabi g++-5-arm-linux-gnueabi libc6-dev-armel-cross linux-headers-generic \
gcc-5-arm-linux-gnueabihf g++-5-arm-linux-gnueabihf libc6-dev-armhf-cross \
gcc-5-aarch64-linux-gnu g++-5-aarch64-linux-gnu libc6-dev-arm64-cross \
gcc-5-mips-linux-gnu g++-5-mips-linux-gnu libc6-dev-mips-cross \
gcc-5-mipsel-linux-gnu g++-5-mipsel-linux-gnu libc6-dev-mipsel-cross \
gcc-5-mips64-linux-gnuabi64 g++-5-mips64-linux-gnuabi64 libc6-dev-mips64-cross \
gcc-5-mips64el-linux-gnuabi64 g++-5-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \
gcc-5-multilib g++-5-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
gcc-6-arm-linux-gnueabi g++-6-arm-linux-gnueabi libc6-dev-armel-cross \
gcc-6-arm-linux-gnueabihf g++-6-arm-linux-gnueabihf libc6-dev-armhf-cross \
gcc-6-aarch64-linux-gnu g++-6-aarch64-linux-gnu libc6-dev-arm64-cross \
gcc-6-mips-linux-gnu g++-6-mips-linux-gnu libc6-dev-mips-cross \
gcc-6-mipsel-linux-gnu g++-6-mipsel-linux-gnu libc6-dev-mipsel-cross \
gcc-6-mips64-linux-gnuabi64 g++-6-mips64-linux-gnuabi64 libc6-dev-mips64-cross \
gcc-6-mips64el-linux-gnuabi64 g++-6-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \
gcc-6-multilib gcc-7-multilib g++-6-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
libtool libxml2-dev uuid-dev libssl-dev swig openjdk-8-jdk pkg-config patch \
make xz-utils cpio wget zip unzip p7zip git mercurial bzr texinfo help2man cmake --no-install-recommends
sudo ln -s /usr/include/asm-generic/ /usr/include/asm
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Go mods
run: |
make test-deps
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Build Binaries
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
run: make build-folders build-linux build-linux-arm build-darwin build-win compress-folders
- name: Upload Builds
uses: actions/upload-artifact@v1
with:
name: builds
path: ./build
upload-release:
needs: [test, test-postman-sqlite, test-postman-mysql, test-postman-postgres, build-binaries]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Builds
uses: actions/download-artifact@v1
with:
name: builds
path: ./builds
- name: Upload Releases
id: upload-assets
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: v${{ env.VERSION }}
draft: false
prerelease: true
files: |
builds/statping-linux-386.tar.gz
builds/statping-linux-amd64.tar.gz
builds/statping-linux-arm.tar.gz
builds/statping-linux-arm64.tar.gz
builds/statping-darwin-amd64.tar.gz
builds/statping-darwin-386.tar.gz
builds/statping-windows-386.zip
builds/statping-windows-amd64.zip
builds/statping-windows-arm.zip
docker-release:
needs: upload-release
needs: [build, test, test-postman-sqlite, test-postman-postgres, test-postman-mysql]
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
@ -412,64 +459,53 @@ jobs:
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Base Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.base
tags: "base"
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v3
- name: Latest/Version Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
- name: Docker Login
env:
VERSION: ${{ env.VERSION }}
ARCH: amd64
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Cache Docker layers
uses: actions/cache@v2
id: buildx-docker
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
tags: "latest,v${{ env.VERSION }}"
buildargs: VERSION,ARCH
path: /tmp/.buildx-cache
key: buildx-docker
restore-keys: |
buildx-docker
- name: Docker Build :base
run: make buildx-base
- name: Docker Build :lastest
run: make buildx-latest
sentry-release:
needs: upload-release
needs: docker-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setup Sentry CLI
uses: mathrix-education/setup-sentry-cli@master
with:
version: latest
url: ${{ secrets.SENTRY_URL }}
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
organization: statping
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Sentry Backend Release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: backend
with:
tagName: v${{ env.VERSION }}
environment: production
- name: Sentry Frontend Release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: frontend
with:
tagName: v${{ env.VERSION }}
environment: production
- name: Sentry Release
run: make sentry-release
homebrew-release:
needs: upload-release
needs: docker-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
@ -486,7 +522,7 @@ jobs:
run: make publish-homebrew
slack-update:
needs: upload-release
needs: docker-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
@ -497,7 +533,7 @@ jobs:
shell: bash
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2.0.0
uses: rtCamp/action-slack-notify@master
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_URL }}
SLACK_CHANNEL: dev

View File

@ -5,9 +5,14 @@ on:
- '*' # matches every branch
- '*/*' # matches every branch containing a single '/'
- '!master' # excludes master
- '!dev' # excludes dev
pull_request:
branches:
- dev
- '*' # matches every branch
- '*/*' # matches every branch containing a single '/'
- '!master' # excludes master
- '!dev' # excludes dev
jobs:
compile:
@ -18,7 +23,7 @@ jobs:
go-version: '1.14.2'
- uses: actions/setup-node@v1
with:
node-version: '10.x'
node-version: '12.18.2'
- uses: actions/checkout@v2
- name: Add GOBIN to PATH
@ -78,10 +83,10 @@ jobs:
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
go-version: 1.15.x
- uses: actions/setup-node@v1
with:
node-version: '10.x'
node-version: 12.18.2
- uses: actions/checkout@v2
- name: Install Global Dependencies
@ -92,7 +97,7 @@ jobs:
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -115,6 +120,7 @@ jobs:
SASS=`which sass` gotestsum --no-summary=skipped --format dots -- -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
DB_CONN: sqlite3
STATPING_DIR: ${{ github.workspace }}
API_SECRET: demopassword123
@ -134,7 +140,7 @@ jobs:
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo "::add-path::/opt/hostedtoolcache/node/12.18.2/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
@ -147,6 +153,7 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping

3
.gitignore vendored
View File

@ -3,6 +3,8 @@ snap
prime
stage
parts
assets_backup
certs
releases
core/rice-box.go
config.yml
@ -37,3 +39,4 @@ tmp
/frontend/cypress/screenshots/
/frontend/cypress/videos/
services.yml
statping.wiki

View File

@ -1,3 +1,97 @@
# 0.90.69 (09-18-2020)
- Fixed issue with service view not loading. #808 #811 #800
# 0.90.68 (09-17-2020)
- Added DB_DSN env for mysql, postgres or sqlite DSN database connection string
- Added READ_ONLY env for a read only connection to the database
- Added Custom OAuth OpenID toggle switch in settings (appends 'openid' in scope)
- Fixed Custom OAuth response_type issue
- Added Configs tab in Settings to edit the config.yml from frontend
# 0.90.67 (09-14-2020)
- Modified core settings to update config.yml on save
- Modified Theme Editor to restart the HTTP router on create/delete (fixing 404's)
# 0.90.66 (09-08-2020)
- Added Import and Export views in Dashboard
- Modified services list sparkline to use start/end of day timestamp
- Modified i18n language files, added go generate script to automatically translate
# 0.90.65 (09-01-2020)
- Fixed issue with dashboard not logging in (notifier panic)
- Modified static email templates to github.com/statping/emails
- Modified Regenerate API function to keep API_SECRET env
- Added DEMO_MODE env variable, if true, 'admin' cannot be deleted
- Modified Service sparklines on Dashboard
- Added modal popup for UI deletes/edits
# 0.90.64 (08-18-2020)
- Modified max-width for container to 1012px, larger UI
- Added failure sparklines in the Services list view
- Added "Update Available" alert on the top of Settings if new version is available
- Added Version and Github Commit hash to left navigation on Settings page
- Added "reason" for failures (will be used for more custom notification messages) [regex, lookup, timeout, connection, close, status_code]
- Added Help page that is generated from Statping's Wiki repo on build
- Modified Service Group failures on index page to show 90 days of failures
- Modified Service view page, updated Latency and Ping charts, added failures below
- Modified Service chart on index page to show ping data along with latency
- Added AWS SNS Notifier
- Modified dashboard services UI
- Modified service.Failures API to include 32 failures (max)
# 0.90.63 (08-17-2020)
- Modified build process to use xgo for all arch builds
- Modified Statping's Push Notifications server notifier to match with Firebase/gorush params
# 0.90.62 (08-07-2020)
- Added Notification logs
- Fixed issues with Notifer After (x) failures for notifications
- Modified notifications to not send on initial startup
- Updated Incident UI
- Added additional testing for notifications
- Modified SCSS/SASS files to be generated from 1, main.scss to main.css
- Modified index page to use /assets directory for assets, (main.css, style.css)
- Modified index page to use CDN asset paths
- Fixed New Checkin form
- Modified email notifier template to be rendered from MJML (using go generate)
- Modified database relationships with services using gorm
- Modified "statping env" command to show user/group ID
- Removed "js" folder when exporting assets, js files are always version of release, not static JS files
# 0.90.61 (07-22-2020)
- Modified sass layouts, organized and split up sections
- Modified Checkins to seconds rather than milliseconds (for cronjob)
- Modified Service View page to show data inside cards
- Fixed issue with uptime_data sending incorrect start/end timestamps
- Modified http cache to bypass if url has a "v" query param
- Added "Static Services" (a fake service that requires you to update the online/offline status)
- Added Update Static Service PATCH route (/api/services/{id})
- Modified SASS api endpoints (base, layout, forms, mixins, mobile, variables)
- Added additional testing
- Modified node version from 10.x to 12.18.2
- Modified Notifier's struct values to be NullString and NullInt to allow empty values
- Added Search ability to Logs in UI
- Fixed issue with Incidents and Checkins not being deleted once service is deleted
# 0.90.60 (07-15-2020)
- Added LETSENCRYPT_ENABLE (boolean) env to enable/disable letsencrypt SSL
# 0.90.59 (07-14-2020)
- Added LetsEncrypt SSL Generator by using LETSENCRYPT_HOST and LETSENCRYPT_EMAIL envs.
- Modified JWT token key to be sha256 of API Secret
- Modified github actions to build multi-arch Docker images
- Added "update" command to install latest version
- Fixed dashboard uptime_data API request to request correct start/time timestamp
# 0.90.58 (07-09-2020)
- Fixed ICMP latency/ping durations
- Fixed webhook notifier
- Modified file structure for Vue admin dashboard components.
- Added Gotify notifier
# 0.90.57 (07-04-2020)
- Fixed login issue
# 0.90.56 (06-25-2020)
- Modified metrics now include service name for each service metric
- Added switch for true/false notifier values

19
CloudronManifest.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "com.statping",
"title": "Statping",
"author": "Hunter Long <info@statping.com>",
"description": "Monitor your web services and remote servers",
"tagline": "Server Monitoring",
"version": "0.90.61",
"healthCheckPath": "/health",
"httpPort": 8080,
"addons": {
"localstorage": {}
},
"manifestVersion": 2,
"website": "https://github.com/statping/statping",
"contactEmail": "info@statping.com",
"icon": "https://assets.statping.com/icon.png",
"tags": [ "monitoring", "uptime" ],
"mediaLinks": [ "https://assets.statping.com/cloudron.png" ]
}s

View File

@ -1,18 +1,18 @@
FROM statping/statping:base AS base
ARG BUILDPLATFORM
# Statping main Docker image that contains all required libraries
FROM alpine:latest
RUN apk --no-cache add libgcc libstdc++ ca-certificates curl jq && update-ca-certificates
COPY --from=base /go/bin/statping /usr/local/bin/
COPY --from=base /usr/local/bin/sass /usr/local/bin/
COPY --from=base /root/sassc/bin/sassc /usr/local/bin/
COPY --from=base /usr/local/share/ca-certificates /usr/local/share/
WORKDIR /app
VOLUME /app
ENV IS_DOCKER=true
ENV SASS=/usr/local/bin/sass
ENV SASS=/usr/local/bin/sassc
ENV STATPING_DIR=/app
ENV PORT=8080

View File

@ -1,5 +1,6 @@
FROM node:10.17.0 AS frontend
RUN npm install yarn -g
FROM node:12.18.2-alpine AS frontend
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG BUILDPLATFORM
WORKDIR /statping
COPY ./frontend/package.json .
COPY ./frontend/yarn.lock .
@ -7,20 +8,28 @@ RUN yarn install --pure-lockfile --network-timeout 1000000
COPY ./frontend .
RUN yarn build && yarn cache clean
# Statping Golang BACKEND building from source
# Creates "/go/bin/statping" and "/usr/local/bin/sass" for copying
FROM golang:1.14-alpine AS backend
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
RUN apk add --update --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq && \
ARG COMMIT
ARG BUILDPLATFORM
ARG TARGETARCH
RUN apk add --update --no-cache libstdc++ gcc g++ make git autoconf \
libtool ca-certificates linux-headers wget curl jq && \
update-ca-certificates
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
WORKDIR /root
RUN git clone https://github.com/sass/sassc.git
RUN . sassc/script/bootstrap && make -C sassc -j4
# sassc binary: /root/sassc/bin/sassc
WORKDIR /go/src/github.com/statping/statping
ADD go.mod go.sum ./
RUN go mod download
ENV GO111MODULE on
ENV CGO_ENABLED 1
RUN go get github.com/stretchr/testify/assert && \
go get github.com/stretchr/testify/require && \
go get github.com/GeertJohan/go.rice/rice && \
@ -28,8 +37,9 @@ RUN go get github.com/stretchr/testify/assert && \
go get github.com/crazy-max/xgo
COPY . .
COPY --from=frontend /statping/dist/ ./source/dist/
RUN make clean frontend-copy generate embed build
RUN make clean generate embed
RUN go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo linux" ./cmd
RUN chmod a+x statping && mv statping /go/bin/statping
# /go/bin/statping - statping binary
# /usr/local/bin/sass - sass binary
# /root/sassc/bin/sassc - sass binary
# /statping - Vue frontend (from frontend)

214
Makefile
View File

@ -1,19 +1,40 @@
VERSION=$(shell cat version.txt)
COMMIT=$(shell git rev-parse HEAD)
SIGN_KEY=B76D61FAA6DB759466E83D9964B9C6AAE2D55278
BINARY_NAME=statping
GOBUILD=go build -a
GOVERSION=1.14.0
NODE_VERSION=12.18.2
XGO=xgo -go $(GOVERSION) --dest=build
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)"
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}"
TRVIS_SECRET=O3/2KTOV8krv+yZ1EB/7D1RQRe6NdpFUEJNJkMS/ollYqmz3x2mCO7yIgIJKCKguLXZxjM6CxJcjlCrvUwibL+8BBp7xJe4XFIOrjkPvbbVPry4HkFZCf2GfcUK6o4AByQ+RYqsW2F17Fp9KLQ1rL3OT3eLTwCAGKx3tlY8y+an43zkmo5dN64V6sawx26fh6XTfww590ey+ltgQTjf8UPNup2wZmGvMo9Hwvh/bYR/47bR6PlBh6vhlKWyotKf2Fz1Bevbu0zc35pee5YlsrHR+oSF+/nNd/dOij34BhtqQikUR+zQVy9yty8SlmneVwD3yOENvlF+8roeKIXb6P6eZnSMHvelhWpAFTwDXq2N3d/FIgrQtLxsAFTI3nTHvZgs6OoTd6dA0wkhuIGLxaL3FOeztCdxP5J/CQ9GUcTvifh5ArGGwYxRxQU6rTgtebJcNtXFISP9CEUR6rwRtb6ax7h6f1SbjUGAdxt+r2LbEVEk4ZlwHvdJ2DtzJHT5DQtLrqq/CTUgJ8SJFMkrJMp/pPznKhzN4qvd8oQJXygSXX/gz92MvoX0xgpNeLsUdAn+PL9KketfR+QYosBz04d8k05E+aTqGaU7FUCHPTLwlOFvLD8Gbv0zsC/PWgSLXTBlcqLEz5PHwPVHTcVzspKj/IyYimXpCSbvu1YOIjyc=
PUBLISH_BODY='{ "request": { "branch": "master", "message": "Homebrew update version v${VERSION}", "config": { "env": { "VERSION": "${VERSION}", "COMMIT": "$(TRAVIS_COMMIT)" } } } }'
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "merge_mode": "replace", "language": "go", "go": 1.14, "install": true, "sudo": "required", "services": ["docker"], "env": { "secure": "${TRVIS_SECRET}" }, "before_deploy": ["git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "git tag v$(VERSION) --force"], "deploy": [{ "provider": "releases", "api_key": "$$GITHUB_TOKEN", "file_glob": true, "file": "build/*", "skip_cleanup": true, "on": { "branch": "master" } }], "before_script": ["rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable", "nvm install 10.17.0", "nvm use 10.17.0 --default", "npm install -g sass yarn cross-env", "pip install --user awscli"], "script": ["make release"], "after_success": [], "after_deploy": ["make post-release"] } } }'
TEST_DIR=$(GOPATH)/src/github.com/statping/statping
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
PATH:=$(GOPATH)/bin:$(PATH)
OS = freebsd linux openbsd
ARCHS = 386 arm amd64 arm64
all: clean yarn-install compile docker-base docker-vue build-all
all: build-deps compile install test build
test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -coverprofile=coverage.out ./...
build: clean
CGO_ENABLED=1 go build -a -ldflags "-s -w -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo osusergo" ./cmd
go-build: clean
rm -rf source/dist
rm -rf source/rice-box.go
wget https://assets.statping.com/source.tar.gz
tar -xvf source.tar.gz
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo osusergo" ./cmd
lint:
go fmt ./...
golint ./...
impi --local github.com/statping/statping/ --scheme stdLocalThirdParty ./...
goimports ./...
up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
@ -29,9 +50,6 @@ lite: clean
reup: down clean compose-build-full up
test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
# build all arch's and release Statping
release: test-deps
wget -O statping.gpg $(SIGN_URL)
@ -50,7 +68,7 @@ cypress: clean
test-api:
DB_CONN=sqlite DB_HOST=localhost DB_DATABASE=sqlite DB_PASS=none DB_USER=none statping &
sleep 5000 && newman run source/tmpl/postman.json -e dev/postman_environment.json --delay-request 500
sleep 5000 && newman run dev/postman.json -e dev/postman_environment.json --delay-request 500
test-deps:
go get golang.org/x/tools/cmd/cover
@ -58,6 +76,7 @@ test-deps:
go get github.com/GeertJohan/go.rice/rice
go get github.com/mattn/go-sqlite3
go install github.com/mattn/go-sqlite3
go install github.com/wellington/go-libsass
deps:
go get -d -v -t ./...
@ -121,14 +140,17 @@ top:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml top
frontend-build:
rm -rf source/dist && rm -rf frontend/dist
@echo "Removing old frontend distributions..."
@rm -rf source/dist && rm -rf frontend/dist
@echo "yarn install and build static frontend"
cd frontend && yarn && yarn build
cp -r frontend/dist source/ && cp -r frontend/src/assets/scss source/dist/
cp -r source/tmpl/*.* source/dist/
cp -r frontend/public/favicon source/dist/
frontend-copy:
cp -r source/tmpl/*.* source/dist/
@cp -r frontend/dist source/
@cp -r frontend/src/assets/scss source/dist/
@cp frontend/public/favicon.ico source/dist/
@cp frontend/public/robots.txt source/dist/
@cp frontend/public/banner.png source/dist/
@cp -r frontend/public/favicon source/dist/
@echo "Frontend build complete at ./source/dist"
yarn:
rm -rf source/dist && rm -rf frontend/dist
@ -138,32 +160,40 @@ yarn:
compile: frontend-build
rm -f source/rice-box.go
cd source && rice embed-go
make generate
embed:
cd source && rice embed-go
build:
$(GOBUILD) $(BUILDVERSION) -o $(BINARY_NAME) ./cmd
install: build
mv $(BINARY_NAME) $(GOPATH)/bin/$(BINARY_NAME)
install-local: build
mv $(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
install-darwin:
go build -a -ldflags "-X main.VERSION=${VERSION}" -o statping --tags "netgo darwin" ./cmd
mv $(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
generate:
cd source && go generate
go generate ./...
build-all: clean compile build-folders build-linux build-linux-arm build-darwin build-win compress-folders
build-win:
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-posix CXX=x86_64-w64-mingw32-g++-posix GO111MODULE="on" GOOS=windows GOARCH=amd64 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-windows-amd64/statping.exe ./cmd
CGO_ENABLED=1 CC=i686-w64-mingw32-gcc-posix CXX=i686-w64-mingw32-g++-posix GO111MODULE="on" GOOS=windows GOARCH=386 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-windows-386/statping.exe ./cmd
build-deps:
apt install -y libc6-armel-cross libc6-dev-armel-cross binutils-arm-linux-gnueabi \
libncurses5-dev build-essential bison flex libssl-dev bc gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \
gcc-arm-linux-gnueabi g++-arm-linux-gnueabi libsqlite3-dev gcc-mingw-w64 gcc-mingw-w64-x86-64
build-darwin:
GO111MODULE="on" GOOS=darwin GOARCH=amd64 go build -a -ldflags "-s -w -X main.VERSION=${VERSION}" -o releases/statping-darwin-amd64/statping --tags "netgo darwin" ./cmd
GO111MODULE="on" GOOS=darwin GOARCH=amd64 \
go build -a -ldflags "-s -w -X main.VERSION=${VERSION}" -o releases/statping-darwin-amd64/statping --tags "netgo darwin" ./cmd
build-win:
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ GO111MODULE="on" GOOS=windows GOARCH=amd64 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-windows-amd64/statping.exe ./cmd
CGO_ENABLED=1 CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ GO111MODULE="on" GOOS=windows GOARCH=386 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-windows-386/statping.exe ./cmd
build-linux:
CGO_ENABLED=1 GO111MODULE="on" GOOS=linux GOARCH=amd64 \
@ -172,16 +202,18 @@ build-linux:
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-386/statping --tags "netgo linux" ./cmd
build-linux-arm:
CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc-6 CXX=arm-linux-gnueabihf-g++-6 GO111MODULE="on" GOOS=linux GOARCH=arm GOARM=6 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-arm6/statping --tags "netgo" ./cmd
CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc-6 CXX=arm-linux-gnueabihf-g++-6 GO111MODULE="on" GOOS=linux GOARCH=arm GOARM=7 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-arm/statping ./cmd
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-arm7/statping --tags "netgo" ./cmd
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc-6 CXX=aarch64-linux-gnu-g++-6 GO111MODULE="on" GOOS=linux GOARCH=arm64 \
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-arm64/statping ./cmd
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o releases/statping-linux-arm64/statping --tags "netgo" ./cmd
build-folders:
mkdir build || true
for os in windows darwin linux;\
do \
for arch in 386 amd64 arm arm64;\
for arch in 386 amd64 arm6 arm7 arm64;\
do \
mkdir -p releases/statping-$$os-$$arch/; \
done \
@ -191,7 +223,7 @@ compress-folders:
mkdir build || true
for os in darwin linux;\
do \
for arch in 386 amd64 arm arm64;\
for arch in 386 amd64 arm6 arm7 arm64;\
do \
chmod +x releases/statping-$$os-$$arch/statping || true; \
tar -czf releases/statping-$$os-$$arch.tar.gz -C releases/statping-$$os-$$arch statping || true; \
@ -208,26 +240,26 @@ compress-folders:
# remove files for a clean compile/build
clean:
rm -rf ./{logs,assets,plugins,*.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,index.html,vendor}
rm -rf cmd/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,*.html,*.json}
rm -rf core/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf types/notifications/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf handlers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf notifiers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf source/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf types/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf utils/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf frontend/{logs,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf dev/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
rm -rf {parts,prime,snap,stage}
rm -rf frontend/cypress/videos
rm -f coverage.* sass
rm -rf **/*.db-journal
rm -rf *.snap
find . -name "*.out" -type f -delete
find . -name "*.cpu" -type f -delete
find . -name "*.mem" -type f -delete
rm -rf {build,releases,tmp,source/build,snap}
@echo "Cleaning temporary and build folders..."
@rm -rf ./{logs,assets,plugins,*.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,index.html,vendor}
@rm -rf cmd/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,*.html,*.json}
@rm -rf core/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf types/notifications/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf handlers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf notifiers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf source/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf types/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf utils/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf frontend/{logs,plugins,*.db,config.yml,.sass-cache,*.log}
@rm -rf dev/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
@rm -rf frontend/cypress/videos
@rm -f coverage.* sass
@rm -rf **/*.db-journal
@find . -name "*.out" -type f -delete
@find . -name "*.cpu" -type f -delete
@find . -name "*.mem" -type f -delete
@rm -rf {build,releases,tmp,source/build,snap,parts,prime,snap,stage}
@echo "Finished removing temporary and build folders"
print_details:
@echo \==== Statping Development Instance ====
@ -279,11 +311,14 @@ post-release: frontend-build upload_to_s3 publish-homebrew dockerhub
publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests
upload_to_s3: travis_s3_creds
aws s3 cp ./source/dist/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/dist/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
aws s3 cp ./source/dist/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT)
upload_to_s3:
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/
rm -rf source.tar.gz
aws s3 cp source/dist/css/ s3://assets.statping.com/css/ --recursive --exclude "*" --include "*.css"
aws s3 cp source/dist/js/ s3://assets.statping.com/js/ --recursive --exclude "*" --include "*.js"
aws s3 cp source/dist/scss/ s3://assets.statping.com/scss/ --recursive --exclude "*" --include "*.scss"
aws s3 cp install.sh s3://assets.statping.com/
travis_s3_creds:
mkdir -p ~/.aws
@ -296,11 +331,25 @@ valid-sign:
gpg --verify statping.asc
sentry-release:
sentry-cli releases new -p backend -p frontend v${VERSION}
sentry-cli releases set-commits --auto v${VERSION}
sentry-cli releases finalize v${VERSION}
sentry-cli releases --org statping --project backend new v${VERSION}
sentry-cli releases --org statping --project backend set-commits v${VERSION} --auto
sentry-cli releases --org statping --project backend finalize v${VERSION}
sentry-cli releases --org statping --project frontend new v${VERSION}
sentry-cli releases --org statping --project frontend set-commits v${VERSION} --auto
sentry-cli releases --org statping --project frontend finalize v${VERSION}
snapcraft: clean compile build-linux
download-bins: clean
mkdir build || true
wget "https://github.com/statping/statping/releases/download/v${VERSION}/statping-linux-386.tar.gz"
wget "https://github.com/statping/statping/releases/download/v${VERSION}/statping-linux-amd64.tar.gz"
wget "https://github.com/statping/statping/releases/download/v${VERSION}/statping-linux-arm.tar.gz"
wget "https://github.com/statping/statping/releases/download/v${VERSION}/statping-linux-arm64.tar.gz"
mv statping-linux-386.tar.gz build/
mv statping-linux-amd64.tar.gz build/
mv statping-linux-arm.tar.gz build/
mv statping-linux-arm64.tar.gz build/
snapcraft: download-bins
mkdir snap
mv snapcraft.yaml snap/
PWD=$(shell pwd)
@ -331,5 +380,54 @@ certs:
-keyout key.pem \
-subj "/C=US/ST=California/L=Santa Monica/O=Statping/OU=Development/CN=localhost"
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
xgo-latest:
xgo --go $(GOVERSION) --targets=linux/amd64,linux/386,linux/arm-7,linux/arm-6,linux/arm64,windows/386,windows/amd64,darwin/386,darwin/amd64 --out='statping' --pkg='cmd' --dest=build --tags 'netgo' --ldflags='-X main.VERSION=${VERSION} -X main.COMMIT=$(COMMIT) -linkmode external -extldflags "-static"' .
buildx-latest: multiarch
docker buildx create --name statping-latest
docker buildx inspect --builder statping-latest --bootstrap
docker buildx build --builder statping-latest --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:latest -t statping/statping:v${VERSION} --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-latest
buildx-dev: multiarch
docker buildx create --name statping-dev
docker buildx inspect --builder statping-dev --bootstrap
docker buildx build --builder statping-dev --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:dev --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-dev
buildx-base: multiarch
docker buildx create --name statping-base
docker buildx inspect --builder statping-base --bootstrap
docker buildx build --builder statping-base --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile.base -t statping/statping:base --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-base
multiarch:
mkdir /tmp/.buildx-cache || true
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
delve:
go build -gcflags "all=-N -l" -o statping ./cmd
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./statping
check:
@echo "Checking the programs required for the build are installed..."
@echo "go: $(shell go version) - $(shell which go)" && go version >/dev/null 2>&1 || (echo "ERROR: go 1.14 is required."; exit 1)
@echo "node: $(shell node --version) - $(shell which node)" && node --version >/dev/null 2>&1 || (echo "ERROR: node 12.x is required."; exit 1)
@echo "yarn: $(shell yarn --version) - $(shell which yarn)" && yarn --version >/dev/null 2>&1 || (echo "ERROR: yarn is required."; exit 1)
@echo "All required programs are installed!"
#sentry-release:
# sentry-cli releases new -p $SENTRY_PROJECT $VERSION
# sentry-cli releases set-commits --auto $VERSION
# sentry-cli releases files $VERSION upload-sourcemaps dist
gen_help:
for file in ./statping.wiki/*.md
do
# convert each file to html and place it in the html directory
# --gfm == use github flavoured markdown
marked -o html/$file.html $file --gfm
done
.PHONY: all check build certs multiarch install-darwin go-build build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds

View File

@ -2,6 +2,6 @@
"name": "Statping",
"description": "Statping Server Monitoring with Status Page",
"repository": "https://github.com/statping/statping",
"logo": "https://raw.githubusercontent.com/statping/statping/master/source/tmpl/banner.png",
"logo": "https://assets.statping.com/banner.png",
"keywords": ["statping", "server", "monitoring", "status page","golang", "go"]
}

View File

@ -5,21 +5,23 @@ import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/statping/statping/handlers"
"github.com/statping/statping/source"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users"
"github.com/statping/statping/utils"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
var (
importAll *bool
)
func assetsCli() error {
dir := utils.Directory
if err := utils.InitLogs(); err != nil {
@ -34,12 +36,81 @@ func assetsCli() error {
return nil
}
func systemctlCli(dir string, uninstall bool, port int64) error {
location := "/etc/systemd/system/statping.service"
if uninstall {
fmt.Println("systemctl stop statping")
if _, _, err := utils.Command("systemctl", "stop", "statping"); err != nil {
log.Errorln(err)
}
fmt.Println("systemctl disable statping")
if _, _, err := utils.Command("systemctl", "disable", "statping"); err != nil {
log.Errorln(err)
}
fmt.Println("Deleting systemctl: ", location)
if err := utils.DeleteFile(location); err != nil {
log.Errorln(err)
}
return nil
}
if ok := utils.FolderExists(dir); !ok {
return errors.New("directory does not exist: " + dir)
}
binPath, err := os.Executable()
if err != nil {
return err
}
config := []byte(`[Unit]
Description=Statping Server
After=network.target
After=systemd-user-sessions.service
After=network-online.target
[Service]
Type=simple
Restart=always
Environment="STATPING_DIR=` + dir + `"
Environment="ALLOW_REPORTS=true"
ExecStart=` + binPath + ` --port=` + utils.ToString(port) + `
WorkingDirectory=` + dir + `
[Install]
WantedBy=multi-user.target"
`)
fmt.Println("Saving systemctl service to: ", location)
fmt.Printf("Using directory %s for Statping data\n", dir)
fmt.Printf("Running on port %d\n", port)
fmt.Printf("\n\n%s\n\n", string(config))
if err := utils.SaveFile(location, config); err != nil {
return err
}
fmt.Println("systemctl daemon-reload")
if _, _, err := utils.Command("systemctl", "daemon-reload"); err != nil {
return err
}
fmt.Println("systemctl enable statping")
if _, _, err := utils.Command("systemctl", "enable", "statping.service"); err != nil {
return err
}
fmt.Println("systemctl start statping")
if _, _, err := utils.Command("systemctl", "start", "statping"); err != nil {
return err
}
fmt.Println("Statping was will auto start on reboots")
fmt.Println("systemctl service: ", location)
return nil
}
func exportCli(args []string) error {
filename := fmt.Sprintf("%s/statping-%s.json", utils.Directory, time.Now().Format("01-02-2006-1504"))
filename := filepath.Join(utils.Directory, time.Now().Format("01-02-2006-1504")+".json")
if len(args) == 1 {
filename = fmt.Sprintf("%s/%s", utils.Directory, args)
}
var data []byte
var data *handlers.ExportData
if err := utils.InitLogs(); err != nil {
return err
}
@ -56,10 +127,10 @@ func exportCli(args []string) error {
if _, err := services.SelectAllServices(false); err != nil {
return err
}
if data, err = ExportSettings(); err != nil {
if data, err = handlers.ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
if err = utils.SaveFile(filename, data); err != nil {
if err = utils.SaveFile(filename, data.JSON()); err != nil {
return fmt.Errorf("could not write file statping-export.json: %v", err.Error())
}
log.Infoln("Statping export file saved to ", filename)
@ -73,7 +144,7 @@ func sassCli() error {
if err := source.Assets(); err != nil {
return err
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
if err := source.CompileSASS(); err != nil {
return err
}
return nil
@ -128,6 +199,10 @@ func resetCli() error {
func envCli() error {
fmt.Println("Statping Configuration")
fmt.Printf("Process ID: %d\n", os.Getpid())
fmt.Printf("Running as user id: %d\n", os.Getuid())
fmt.Printf("Running as group id: %d\n", os.Getgid())
fmt.Printf("Statping Directory: %s\n", utils.Directory)
for k, v := range utils.Params.AllSettings() {
fmt.Printf("%s=%v\n", strings.ToUpper(k), v)
}
@ -153,20 +228,54 @@ func onceCli() error {
func importCli(args []string) error {
var err error
var data []byte
filename := args[1]
if data, err = ioutil.ReadFile(filename); err != nil {
if len(args) < 1 {
return errors.New("invalid command arguments")
}
if data, err = ioutil.ReadFile(args[0]); err != nil {
return err
}
var exportData ExportData
var exportData handlers.ExportData
if err = json.Unmarshal(data, &exportData); err != nil {
return err
}
log.Printf("=== %s ===\n", exportData.Core.Name)
log.Printf("Services: %d\n", len(exportData.Services))
log.Printf("Checkins: %d\n", len(exportData.Checkins))
log.Printf("Groups: %d\n", len(exportData.Groups))
log.Printf("Messages: %d\n", len(exportData.Messages))
log.Printf("Users: %d\n", len(exportData.Users))
if exportData.Config != nil {
log.Printf("Configs: %s\n", exportData.Config.DbConn)
if exportData.Config.DbUser != "" {
log.Printf(" - Host: %s\n", exportData.Config.DbHost)
log.Printf(" - User: %s\n", exportData.Config.DbUser)
}
}
if len(exportData.Services) > 0 {
log.Printf("Services: %d\n", len(exportData.Services))
}
if len(exportData.Checkins) > 0 {
log.Printf("Checkins: %d\n", len(exportData.Checkins))
}
if len(exportData.Groups) > 0 {
log.Printf("Groups: %d\n", len(exportData.Groups))
}
if len(exportData.Messages) > 0 {
log.Printf("Messages: %d\n", len(exportData.Messages))
}
if len(exportData.Incidents) > 0 {
log.Printf("Incidents: %d\n", len(exportData.Incidents))
}
if len(exportData.Users) > 0 {
log.Printf("Users: %d\n", len(exportData.Users))
}
if exportData.Config != nil {
if ask("Create config.yml file from Configs?") {
log.Printf("Database Host: %s\n", exportData.Config.DbHost)
log.Printf("Database Port: %d\n", exportData.Config.DbPort)
log.Printf("Database User: %s\n", exportData.Config.DbUser)
log.Printf("Database Password: %s\n", exportData.Config.DbPass)
if err := exportData.Config.Save(utils.Directory); err != nil {
return err
}
}
}
config, err := configs.LoadConfigs(configFile)
if err != nil {
@ -175,21 +284,22 @@ func importCli(args []string) error {
if err = configs.ConnectConfigs(config, false); err != nil {
return err
}
if data, err = ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
if ask("Create database rows and sample data?") {
if err := config.ResetCore(); err != nil {
return err
}
}
if ask("Import Core settings?") {
c := exportData.Core
if err := c.Update(); err != nil {
return err
log.Errorln(err)
}
}
for _, s := range exportData.Groups {
if ask(fmt.Sprintf("Import Group '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
log.Errorln(err)
}
}
}
@ -197,7 +307,7 @@ func importCli(args []string) error {
if ask(fmt.Sprintf("Import Service '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
log.Errorln(err)
}
}
}
@ -205,7 +315,7 @@ func importCli(args []string) error {
if ask(fmt.Sprintf("Import Checkin '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
log.Errorln(err)
}
}
}
@ -213,7 +323,7 @@ func importCli(args []string) error {
if ask(fmt.Sprintf("Import Message '%s'?", s.Title)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
log.Errorln(err)
}
}
}
@ -221,7 +331,7 @@ func importCli(args []string) error {
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
log.Errorln(err)
}
}
}
@ -230,6 +340,7 @@ func importCli(args []string) error {
}
func ask(format string) bool {
fmt.Printf(fmt.Sprintf(format + " [y/N]: "))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
@ -375,15 +486,16 @@ type gitUploader struct {
// ExportChartsJs renders the charts for the index page
type ExportData struct {
Core *core.Core `json:"core"`
Services []services.Service `json:"services"`
Messages []*messages.Message `json:"messages"`
Checkins []*checkins.Checkin `json:"checkins"`
Users []*users.User `json:"users"`
Groups []*groups.Group `json:"groups"`
Notifiers []core.AllNotifiers `json:"notifiers"`
}
//type ExportData struct {
// Config *configs.DbConfig `json:"config"`
// Core *core.Core `json:"core"`
// Services []services.Service `json:"services"`
// Messages []*messages.Message `json:"messages"`
// Checkins []*checkins.Checkin `json:"checkins"`
// Users []*users.User `json:"users"`
// Groups []*groups.Group `json:"groups"`
// Notifiers []core.AllNotifiers `json:"notifiers"`
//}
// ExportSettings will export a JSON file containing all of the settings below:
// - Core
@ -393,28 +505,35 @@ type ExportData struct {
// - Services
// - Groups
// - Messages
func ExportSettings() ([]byte, error) {
c, err := core.Select()
if err != nil {
return nil, err
}
var srvs []services.Service
for _, s := range services.AllInOrder() {
s.Failures = nil
srvs = append(srvs, s)
}
data := ExportData{
Core: c,
Notifiers: core.App.Notifications,
Checkins: checkins.All(),
Users: users.All(),
Services: srvs,
Groups: groups.All(),
Messages: messages.All(),
}
export, err := json.Marshal(data)
return export, err
}
//func ExportSettings() ([]byte, error) {
// c, err := core.Select()
// if err != nil {
// return nil, err
// }
// var srvs []services.Service
// for _, s := range services.AllInOrder() {
// s.Failures = nil
// srvs = append(srvs, s)
// }
//
// cfg, err := configs.LoadConfigs(configFile)
// if err != nil {
// return nil, err
// }
//
// data := ExportData{
// Config: cfg,
// Core: c,
// Notifiers: core.App.Notifications,
// Checkins: checkins.All(),
// Users: users.All(),
// Services: srvs,
// Groups: groups.All(),
// Messages: messages.All(),
// }
// export, err := json.Marshal(data)
// return export, err
//}
// ExportIndexHTML returns the HTML of the index page as a string
//func ExportIndexHTML() []byte {

View File

@ -2,10 +2,12 @@ package main
import (
"bytes"
"github.com/statping/statping/source"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"testing"
)
@ -18,16 +20,15 @@ func init() {
}
func TestStatpingDirectory(t *testing.T) {
dir := utils.Directory
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
dir = utils.Params.GetString("STATPING_DIR")
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
}
func TestEnvCLI(t *testing.T) {
os.Setenv("API_SECRET", "demoapisecret123")
os.Setenv("SASS", "/usr/local/bin/sass")
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
@ -39,6 +40,12 @@ func TestEnvCLI(t *testing.T) {
assert.Contains(t, string(out), VERSION)
assert.Contains(t, utils.Directory, string(out))
assert.Contains(t, "SAMPLE_DATA=true", string(out))
assert.Contains(t, "API_SECRET=demoapisecret123", string(out))
assert.Contains(t, "STATPING_DIR="+dir, string(out))
assert.Contains(t, "SASS=/usr/local/bin/sass", string(out))
os.Unsetenv("API_SECRET")
os.Unsetenv("SASS")
}
func TestVersionCLI(t *testing.T) {
@ -58,16 +65,26 @@ func TestAssetsCLI(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"assets"})
cmd.Execute()
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
assert.Nil(t, err)
assert.Contains(t, string(out), VERSION)
assert.FileExists(t, utils.Directory+"/assets/css/main.css")
assert.FileExists(t, utils.Directory+"/assets/css/style.css")
assert.FileExists(t, utils.Directory+"/assets/css/vendor.css")
assert.FileExists(t, utils.Directory+"/assets/scss/base.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/mobile.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/variables.scss")
for _, f := range source.RequiredFiles {
assert.FileExists(t, utils.Directory+"/assets/"+f)
}
}
func TestUpdateCLI(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"update"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, string(out), VERSION)
}
func TestHelpCLI(t *testing.T) {

View File

@ -1,23 +1,70 @@
package main
import (
"bytes"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/statping/statping/utils"
"io"
"os"
"os/exec"
)
var rootCmd = &cobra.Command{
Use: "statping",
Short: "A simple Application Status Monitor that is opensource and lightweight.",
Use: "statping",
Version: VERSION,
Short: "A simple Application Status Monitor that is opensource and lightweight.",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
var updateCmd = &cobra.Command{
Use: "update",
Example: "statping update",
Short: "Update to the latest version",
RunE: func(cmd *cobra.Command, args []string) error {
log.Infoln("Updating Statping to the latest version...")
log.Infoln("curl -o- -L https://statping.com/install.sh | bash")
curl, err := exec.LookPath("curl")
if err != nil {
return err
}
bash, err := exec.LookPath("bash")
if err != nil {
return err
}
ree := bytes.NewBuffer(nil)
c1 := exec.Command(curl, "-o-", "-L", "https://statping.com/install.sh")
c2 := exec.Command(bash)
r, w := io.Pipe()
c1.Stdout = w
c2.Stdin = r
var b2 bytes.Buffer
c2.Stdout = &b2
c1.Start()
c2.Start()
c1.Wait()
w.Close()
c2.Wait()
io.Copy(ree, &b2)
log.Infoln(ree.String())
os.Exit(0)
return nil
},
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Statping",
Use: "version",
Example: "statping version",
Short: "Print the version number of Statping",
Run: func(cmd *cobra.Command, args []string) {
if COMMIT != "" {
fmt.Printf("%s (%s)\n", VERSION, COMMIT)
@ -28,9 +75,38 @@ var versionCmd = &cobra.Command{
},
}
var systemctlCmd = &cobra.Command{
Use: "systemctl [install/uninstall]",
Example: "statping systemctl install",
Short: "Install or Uninstall systemctl services",
RunE: func(cmd *cobra.Command, args []string) error {
if args[1] == "install" {
if len(args) < 3 {
return errors.New("requires 'install <working_path> <port>'")
}
}
port := utils.ToInt(args[2])
if port == 0 {
port = 80
}
if err := systemctlCli(args[1], args[0] == "uninstall", port); err != nil {
return err
}
os.Exit(0)
return nil
},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return errors.New("requires 'install <working_path>' or 'uninstall' as arguments")
}
return nil
},
}
var assetsCmd = &cobra.Command{
Use: "assets",
Short: "Dump all assets used locally to be edited",
Use: "assets",
Example: "statping assets",
Short: "Dump all assets used locally to be edited",
RunE: func(cmd *cobra.Command, args []string) error {
if err := assetsCli(); err != nil {
return err
@ -41,8 +117,9 @@ var assetsCmd = &cobra.Command{
}
var exportCmd = &cobra.Command{
Use: "export",
Short: "Exports your Statping settings to a 'statping-export.json' file.",
Use: "export",
Example: "statping export",
Short: "Exports your Statping settings to a 'statping-export.json' file.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := exportCli(args); err != nil {
return err
@ -53,8 +130,9 @@ var exportCmd = &cobra.Command{
}
var sassCmd = &cobra.Command{
Use: "sass",
Short: "Compile .scss files into the css directory",
Use: "sass",
Example: "statping sass",
Short: "Compile .scss files into the css directory",
RunE: func(cmd *cobra.Command, args []string) error {
if err := sassCli(); err != nil {
return err
@ -65,8 +143,9 @@ var sassCmd = &cobra.Command{
}
var envCmd = &cobra.Command{
Use: "env",
Short: "Return the configs that will be ran",
Use: "env",
Example: "statping env",
Short: "Return the configs that will be ran",
RunE: func(cmd *cobra.Command, args []string) error {
if err := envCli(); err != nil {
return err
@ -77,8 +156,9 @@ var envCmd = &cobra.Command{
}
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Start a fresh copy of Statping",
Use: "reset",
Example: "statping reset",
Short: "Start a fresh copy of Statping",
RunE: func(cmd *cobra.Command, args []string) error {
if err := resetCli(); err != nil {
return err
@ -89,8 +169,9 @@ var resetCmd = &cobra.Command{
}
var onceCmd = &cobra.Command{
Use: "once",
Short: "Check all services 1 time and then quit",
Use: "once",
Example: "statping once",
Short: "Check all services 1 time and then quit",
RunE: func(cmd *cobra.Command, args []string) error {
if err := onceCli(); err != nil {
return err
@ -101,8 +182,9 @@ var onceCmd = &cobra.Command{
}
var importCmd = &cobra.Command{
Use: "import [.json file]",
Short: "Imports settings from a previously saved JSON file.",
Use: "import [.json file]",
Example: "statping import backup.json",
Short: "Imports settings from a previously saved JSON file.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := importCli(args); err != nil {
return err

View File

@ -29,16 +29,20 @@ var (
func init() {
stopped = make(chan bool, 1)
core.New(VERSION)
core.New(VERSION, COMMIT)
utils.InitEnvs()
utils.Params.Set("VERSION", VERSION)
utils.Params.Set("COMMIT", COMMIT)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(updateCmd)
rootCmd.AddCommand(assetsCmd)
rootCmd.AddCommand(exportCmd)
rootCmd.AddCommand(importCmd)
rootCmd.AddCommand(sassCmd)
rootCmd.AddCommand(onceCmd)
rootCmd.AddCommand(envCmd)
rootCmd.AddCommand(systemctlCmd)
rootCmd.AddCommand(resetCmd)
parseFlags(rootCmd)
@ -96,39 +100,8 @@ func start() {
exit(err)
}
if !confgs.Db.HasTable("core") {
var srvs int64
if confgs.Db.HasTable(&services.Service{}) {
confgs.Db.Model(&services.Service{}).Count(&srvs)
if srvs > 0 {
exit(errors.Wrap(err, "there are already services setup."))
return
}
}
if err := confgs.DropDatabase(); err != nil {
exit(errors.Wrap(err, "error dropping database"))
}
if err := confgs.CreateDatabase(); err != nil {
exit(errors.Wrap(err, "error creating database"))
}
if err := configs.CreateAdminUser(confgs); err != nil {
exit(errors.Wrap(err, "error creating default admin user"))
}
if utils.Params.GetBool("SAMPLE_DATA") {
log.Infoln("Adding Sample Data")
if err := configs.TriggerSamples(); err != nil {
exit(errors.Wrap(err, "error adding sample data"))
}
} else {
if err := core.Samples(); err != nil {
exit(errors.Wrap(err, "error added core details"))
}
}
if err = confgs.ResetCore(); err != nil {
exit(err)
}
if err = confgs.DatabaseChanges(); err != nil {
@ -175,20 +148,20 @@ func InitApp() error {
if _, err := core.Select(); err != nil {
return err
}
// init Sentry error monitoring (its useful)
utils.SentryInit(core.App.AllowReports.Bool)
// init prometheus metrics
metrics.InitMetrics()
// connect each notifier, added them into database if needed
notifiers.InitNotifiers()
// select all services in database and store services in a mapping of Service pointers
if _, err := services.SelectAllServices(true); err != nil {
return err
}
// start routines for each service checking process
services.CheckServices()
// connect each notifier, added them into database if needed
notifiers.InitNotifiers()
// start routine to delete old records (failures, hits)
go database.Maintenance()
// init Sentry error monitoring (its useful)
utils.SentryInit(&VERSION, core.App.AllowReports.Bool)
core.App.Setup = true
core.App.Started = utils.Now()
return nil

View File

@ -169,10 +169,6 @@ func Available(db Database) bool {
return true
}
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("service = ?", 1000)
}
func (it *Db) MultipleSelects(args ...string) Database {
joined := strings.Join(args, ", ")
return it.Select(joined)
@ -181,6 +177,7 @@ func (it *Db) MultipleSelects(args ...string) Database {
type Db struct {
Database *gorm.DB
Type string
ReadOnly bool
}
// Openw is a drop-in replacement for Open()
@ -223,6 +220,9 @@ func OpenTester() (Database, error) {
default:
dbString = fmt.Sprintf("file:%s?mode=memory&cache=shared", utils.RandomString(12))
}
if utils.Params.IsSet("DB_DSN") {
dbString = utils.Params.GetString("DB_DSN")
}
newDb, err := Openw(testDB, dbString)
if err != nil {
return nil, err
@ -239,6 +239,7 @@ func Wrap(db *gorm.DB) Database {
return &Db{
Database: db,
Type: db.Dialect().GetName(),
ReadOnly: utils.Params.GetBool("READ_ONLY"),
}
}
@ -379,14 +380,26 @@ func (it *Db) Related(value interface{}, foreignKeys ...string) Database {
}
func (it *Db) FirstOrInit(out interface{}, where ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.FirstOrInit(out, where...))
}
func (it *Db) FirstOrCreate(out interface{}, where ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.FirstOrCreate(out, where...))
}
func (it *Db) Update(attrs ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Update(attrs...))
}
@ -395,22 +408,42 @@ func (it *Db) Updates(values interface{}, ignoreProtectedAttrs ...bool) Database
}
func (it *Db) UpdateColumn(attrs ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.UpdateColumn(attrs...))
}
func (it *Db) UpdateColumns(values interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.UpdateColumns(values))
}
func (it *Db) Save(value interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Save(value))
}
func (it *Db) Create(value interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Create(value))
}
func (it *Db) Delete(value interface{}, where ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Delete(value, where...))
}
@ -435,14 +468,26 @@ func (it *Db) Debug() Database {
}
func (it *Db) Begin() Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Begin())
}
func (it *Db) Commit() Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Commit())
}
func (it *Db) Rollback() Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.Rollback())
}
@ -455,14 +500,26 @@ func (it *Db) RecordNotFound() bool {
}
func (it *Db) CreateTable(values ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.CreateTable(values...))
}
func (it *Db) DropTable(values ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.DropTable(values...))
}
func (it *Db) DropTableIfExists(values ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.DropTableIfExists(values...))
}
@ -471,26 +528,50 @@ func (it *Db) HasTable(value interface{}) bool {
}
func (it *Db) AutoMigrate(values ...interface{}) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.AutoMigrate(values...))
}
func (it *Db) ModifyColumn(column string, typ string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.ModifyColumn(column, typ))
}
func (it *Db) DropColumn(column string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.DropColumn(column))
}
func (it *Db) AddIndex(indexName string, columns ...string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.AddIndex(indexName, columns...))
}
func (it *Db) AddUniqueIndex(indexName string, columns ...string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.AddUniqueIndex(indexName, columns...))
}
func (it *Db) RemoveIndex(indexName string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.RemoveIndex(indexName))
}
@ -519,6 +600,10 @@ func (it *Db) SetJoinTableHandler(source interface{}, column string, handler gor
}
func (it *Db) AddForeignKey(field string, dest string, onDelete string, onUpdate string) Database {
if it.ReadOnly {
it.Database.Error = nil
return Wrap(it.Database)
}
return Wrap(it.Database.AddForeignKey(field, dest, onDelete, onUpdate))
}

View File

@ -39,7 +39,7 @@ type GroupQuery struct {
}
func (b GroupQuery) Find(data interface{}) error {
return b.db.Find(data).Error()
return b.db.Order("id DESC").Find(data).Error()
}
func (b GroupQuery) Database() Database {
@ -70,27 +70,28 @@ func (t *TimeVar) ToValues() ([]*TimeValue, error) {
}
// GraphData will return all hits or failures
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
func (b *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
b.db = b.db.MultipleSelects(
b.db.SelectByTime(b.Group),
by.String(),
).Group("timeframe").Order("timeframe", true)
g.db = dbQuery
caller, err := g.ToTimeValue()
caller, err := b.ToTimeValue()
if err != nil {
return nil, err
}
if g.FillEmpty {
return caller.FillMissing(g.Start, g.End)
if b.FillEmpty {
return caller.FillMissing(b.Start, b.End)
}
return caller.ToValues()
}
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows()
// ToTimeValue will format the SQL rows into a JSON format for the API.
// [{"timestamp": "2006-01-02T15:04:05Z", "amount": 468293}]
// TODO redo this entire function, use better SQL query to group by time
func (b *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := b.db.Rows()
if err != nil {
return nil, err
}
@ -101,8 +102,8 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
if err := rows.Scan(&timeframe, &amount); err != nil {
log.Error(err, timeframe)
}
trueTime, _ := g.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, g.Group)
trueTime, _ := b.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, b.Group)
tv := &TimeValue{
Timeframe: newTs,
@ -110,33 +111,32 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
}
data = append(data, tv)
}
return &TimeVar{g, data}, nil
return &TimeVar{b, data}, nil
}
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
timeMap := make(map[string]int64)
var validSet []*TimeValue
dur := t.g.Group
for _, v := range t.data {
timeMap[v.Timeframe] = v.Amount
}
currentStr := types.FixedTime(current, t.g.Group)
for {
currentStr := types.FixedTime(current, t.g.Group)
var amount int64
if timeMap[currentStr] != 0 {
amount = timeMap[currentStr]
}
validSet = append(validSet, &TimeValue{
Timeframe: currentStr,
Amount: amount,
})
current = current.Add(t.g.Group)
if current.After(end) {
break
}
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.Group)
}
return validSet, nil
@ -233,10 +233,6 @@ func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
if endField == 0 {
query.End = utils.Now()
}
if query.End.After(utils.Now()) {
query.End = utils.Now()
}
if query.Limit != 0 {
q = q.Limit(query.Limit)
}

View File

@ -16,6 +16,8 @@ var (
// Maintenance will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
// env: REMOVE_AFTER - golang duration parsed time for deleting records older than REMOVE_AFTER duration from now
// env: CLEANUP_INTERVAL - golang duration parsed time for checking old records routine
func Maintenance() {
dur := utils.Params.GetDuration("REMOVE_AFTER")
interval := utils.Params.GetDuration("CLEANUP_INTERVAL")

View File

@ -19,10 +19,9 @@ func (it *Db) ParseTime(t string) (time.Time, error) {
}
}
// FormatTime returns the timestamp in the same format as the DATETIME column in database
func (it *Db) FormatTime(t time.Time) string {
switch it.Type {
case "mysql":
return t.Format("2006-01-02 15:04:05")
case "postgres":
return t.Format("2006-01-02 15:04:05.999999999")
default:
@ -30,6 +29,7 @@ func (it *Db) FormatTime(t time.Time) string {
}
}
// SelectByTime returns an SQL query that will group "created_at" column by x seconds and returns as "timeframe"
func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int64(increment.Seconds())
switch it.Type {
@ -41,33 +41,3 @@ func (it *Db) SelectByTime(increment time.Duration) string {
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
}
}
func (it *Db) correctTimestamp(increment string) string {
var timestamper string
switch increment {
case "second":
timestamper = "%Y-%m-%d %H:%M:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%M:00"
case "hour":
timestamper = "%Y-%m-%d %H:00:00"
case "day":
timestamper = "%Y-%m-%d 00:00:00"
case "month":
timestamper = "%Y-%m-01 00:00:00"
case "year":
timestamper = "%Y-01-01 00:00:00"
default:
timestamper = "%Y-%m-%d 00:00:00"
}
switch it.Type {
case "mysql":
case "second":
timestamper = "%Y-%m-%d %H:%i:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%i:00"
}
return timestamper
}

42
dev/kubernetes.yml vendored Normal file
View File

@ -0,0 +1,42 @@
apiVersion: v1
kind: Service
metadata:
name: statping
spec:
ports:
- port: 8080
selector:
app: statping
clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: statping
spec:
selector:
matchLabels:
app: statping
strategy:
type: Recreate
template:
metadata:
labels:
app: statping
spec:
containers:
- image: statping/statping
name: statping
env:
- name: ALLOW_REPORTS
value: "true"
ports:
- containerPort: 8080
name: statping
volumeMounts:
- name: statping-storage
mountPath: /app
volumes:
- name: statping-storage
persistentVolumeClaim:
claimName: statping-claim

21
dev/portainer.json vendored Normal file
View File

@ -0,0 +1,21 @@
[
{
"type": 1,
"title": "Statping",
"restart_policy": "unless-stopped",
"description": "Service monitoring with an easy to use status page and mobile app",
"logo": "https://assets.statping.com/icon.png",
"image": "statping/statping:latest",
"platform": "linux",
"categories": ["monitoring"],
"administrator_only": false,
"ports": [
"8080:8080/tcp"
],
"volumes": [
{
"container": "/app"
}
]
}
]

736
dev/postman.json vendored
View File

@ -2,7 +2,7 @@
"info": {
"_postman_id": "3c6a7841-0b39-4878-a3a6-1c76832b7679",
"name": "Statping",
"description": "The Statping API allows you to programmatically access data on your Statping server.\n\nThe easiest way to get started with the API is by running your own Docker or local instance of the Statping server.\n\n<img width=\"100%\" src=\"https://img.cjx.io/statuptokenbalance.gif\">\n\n# Authentication\n\nAn API Key is required to be sent as part of every request to the Statping API, by using the `Authorization` and the API Secret Key as a header.\n\n> If you do not have an API Secret Key, you can find it on the Settings page.\n\nYou can Authenticate by implementing the following...\n- Using the `Authorization` header with API Secret Key\n- Setting `GO_ENV` to `test` to bypass all authentication\n- Adding `?api=<keyhere>` URL Query along with the API Secret Key\n- Being logged into Statping as an admin (using JWT sessions/cookies)\n\n> Authentication will create a cookie named `statping_auth`.\n\n# Environment Variables\nStatping includes many environment variables that can give you more control over your instance. Please view the [Environment Variables Wiki](https://github.com/statping/statping/wiki/Environment-Variables) page to view a complete list. Below are a couple important ones...\n- `STATPING_DIR` - Statping's working directory. By default, this will be set to the current working directory. This path will contain the `config.yml` file, `logs`, and `assets` folder. \n- `SASS` - Absolute path to the `sass` executable. By default it will attempt to find `sass` in your `$PATH`. \n\n# Demo\nYou can checkout the Statping Demo instance at [https://demo.statping.com](https://demo.statping.com). All features are available for you to experiment with. The API Secret Key is `demoapisecret123`. Since this instance is public, it will be reset with sample data **every 90 minutes**.\n",
"description": "The Statping API allows you to programmatically access data on your Statping server.\n\nThe easiest way to get started with the API is by running your own Docker or local instance of the Statping server.\n\n<img width=\"100%\" src=\"https://img.cjx.io/statuptokenbalance.gif\">\n\n# Authentication\n\nAn API Key is required to be sent as part of every request to the Statping API, by using the `Authorization` and the API Secret Key as a header.\n\n> If you do not have an API Secret Key, you can find it on the Settings page.\n\nYou can Authenticate by implementing the following...\n- Using the `Authorization` header with API Secret Key\n- Setting `GO_ENV` to `test` to bypass all authentication\n- Adding `?api=<keyhere>` URL Query along with the API Secret Key\n- Adding `?api=<userkey>` URL Query along with the API Key for a user\n- Being logged into Statping as an admin (using JWT sessions/cookies)\n\n> Authentication will create a cookie named `statping_auth`.\n\n# Environment Variables\nStatping includes many environment variables that can give you more control over your instance. Please view the [Environment Variables Wiki](https://github.com/statping/statping/wiki/Environment-Variables) page to view a complete list. Below are a couple important ones...\n- `STATPING_DIR` - Statping's working directory. By default, this will be set to the current working directory. This path will contain the `config.yml` file, `logs`, and `assets` folder. \n- `SASS` - Absolute path to the `sass` executable. By default it will attempt to find `sass` in your `$PATH`. \n\n# Demo\nYou can checkout the Statping Demo instance at [https://demo.statping.com](https://demo.statping.com). All features are available for you to experiment with. The API Secret Key is `demoapisecret123`. Since this instance is public, it will be reset with sample data **every 90 minutes**.\n",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
@ -786,6 +786,105 @@
"description": "You can create custom badges with dynamic information by using [Shields.io](https://shields.io/) and parsing JSON fields with [JSONPath](http://jsonpath.com/). \n\n## Examples\n\n#### Service Uptime Percent\n<img src=\"https://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\">\n\n- URL: [https://demo.statping.com/api/services/2](https://demo.statping.com/api/services/2)\n- JSON Path: `$.online_24_hours`\n- Suffix: `%`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\n```\n\n#### Count Services\n<img src=\"https://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\">\n\n- URL: [https://demo.statping.com/health](https://demo.statping.com/health)\n- JSON Path: `$.services`\n- Suffix: ` services`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\n```"
},
"response": []
},
{
"name": "Send Push Notification",
"event": [
{
"listen": "test",
"script": {
"id": "11fe392f-3636-4d2d-84e9-1119b351d8ee",
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"notifications\": [\n {\n \"tokens\": [\"dBLB1WvTJkiWl3ZPjP0-BS:APA91bGXUbKy65CaN1XqExHXZ892jik2k9XORXSiqdUyXhcQ5RDiJ6LfXrckuH3StYJFcma4UCDr_N038YUtxYsRIHYx_8vWZ6D2uq3199LegWXGl5tz-9zk3M4WZGX8WGxIRUJ31QtW\"],\n \"platform\": 2,\n \"message\": \"This notification will go to iOS and Android platform via Firebase!\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "https://push.statping.com/api/push",
"protocol": "https",
"host": [
"push",
"statping",
"com"
],
"path": [
"api",
"push"
]
},
"description": "Send a push notification to the Statping mobile app using your Firebase device identifier."
},
"response": [
{
"name": "Send Push Notification",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"notifications\": [\n {\n \"tokens\": [\"dBLB1WvTJkiWl3ZPjP0-BS:APA91bGXUbKy65CaN1XqExHXZ892jik2k9XORXSiqdUyXhcQ5RDiJ6LfXrckuH3StYJFcma4UCDr_N038YUtxYsRIHYx_8vWZ6D2uq3199LegWXGl5tz-9zk3M4WZGX8WGxIRUJ31QtW\"],\n \"platform\": 2,\n \"message\": \"This notification will go to iOS and Android Statping App\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "https://push.statping.com/api/push",
"protocol": "https",
"host": [
"push",
"statping",
"com"
],
"path": [
"api",
"push"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Length",
"value": "37"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Thu, 13 Aug 2020 02:24:17 GMT"
},
{
"key": "X-Gorush-Version",
"value": "No Version Provided"
}
],
"cookie": [],
"body": "{\n \"counts\": 1,\n \"logs\": [],\n \"success\": \"ok\"\n}"
}
]
}
],
"description": "This is for Statping's miscellaneous API endpoints that aren't a part of another category.",
@ -840,7 +939,7 @@
"",
"pm.test(\"View All Services\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.length).to.eql(5);",
" pm.expect(jsonData.length).to.eql(6);",
"});"
],
"type": "text/javascript"
@ -1714,7 +1813,7 @@
"services"
]
},
"description": "Create a new service and begin monitoring."
"description": "View a specific service, this will include the service's failures and checkins.\n\n#### Service Type Field\n- `http` - HTTP Service\n- `tcp` - TCP Service\n- `udp` - UDP Service\n- `icmp` - ICMP Service\n- `grpc` - gRPC Service\n- `static` - Static Service"
},
"response": [
{
@ -1876,6 +1975,69 @@
}
]
},
{
"name": "Update Static Service",
"event": [
{
"listen": "test",
"script": {
"id": "18cfae1e-4025-4338-a734-a552c8ac85ca",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Update Service\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.status).to.eql(\"success\");",
" pm.expect(jsonData.output.type).to.eql(\"static\");",
" pm.expect(jsonData.output.online).to.eql(false);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "PATCH",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"online\": false,\n \"latency\": 30500,\n \"issue\": \"This is a failure string you can create\"\n}",
"options": {
"raw": {}
}
},
"url": {
"raw": "{{endpoint}}/api/services/7",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"services",
"7"
]
},
"description": "Update a Static Service by setting it's online status to true or false. If false, you can include a issue string in the `issue` JSON field."
},
"response": []
},
{
"name": "Delete Service Failures",
"event": [
@ -2026,7 +2188,7 @@
]
}
],
"description": "With the Statping API, you can add, remove, edit all your services fields from the API directly. This includes viewing Service chart data for latency/up-time, and even viewing a log of failures. ",
"description": "With the Statping API, you can add, remove, edit all your services fields from the API directly. This includes viewing Service chart data for latency/up-time, and even viewing a log of failures. \n\n### HTTP Services\nA HTTP service is a basic service that sends a HTTP request (GET, POST, PATCH, DELETE, etc) to check if that web service is online or not. You can expect a specific status code, and response body (including regex). \n\n### TCP and UDP Services\nTCP and UDP Services will send a request to the hostname and port of your choice.\n\n### ICMP Ping Services\nICMP Services will send a ICMP (ping) packet to your server to test if it's online.\n\n### gRPC Services\ngRPC Services will request your gRPC server and check the response\n\n### Static Services\nA Static Service is a \"fake\" service that is set online/offline by you.\n",
"auth": {
"type": "bearer",
"bearer": [
@ -2218,14 +2380,14 @@
}
},
"url": {
"raw": "{{endpoint}}/api/services/1/incidents",
"raw": "{{endpoint}}/api/services/3/incidents",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"services",
"1",
"3",
"incidents"
]
},
@ -2292,213 +2454,6 @@
}
]
},
{
"name": "Update Service Incident",
"event": [
{
"listen": "test",
"script": {
"id": "42cd68de-5bd4-4d4e-b687-fa0bafc1d61a",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"title\": \"Updated Downtime\",\n\t\"description\": \"This is an update for an incident\"\n}"
},
"url": {
"raw": "{{endpoint}}/api/incidents/1",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"1"
]
},
"description": "View all incidents for a single service."
},
"response": []
},
{
"name": "Delete Incident",
"event": [
{
"listen": "test",
"script": {
"id": "c6735dbe-86e8-4b42-9b04-6fc1fea949df",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/1",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"1"
]
},
"description": "View all incidents for a single service."
},
"response": [
{
"name": "Delete Incident",
"originalRequest": {
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/2",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"2"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Date",
"value": "Mon, 04 May 2020 03:31:48 GMT"
},
{
"key": "Content-Length",
"value": "207"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"status\": \"success\",\n \"type\": \"incident\",\n \"method\": \"delete\",\n \"id\": 2,\n \"output\": {\n \"id\": 2,\n \"title\": \"Service Downtime\",\n \"service\": 1,\n \"created_at\": \"2020-05-04T03:18:24.818629Z\",\n \"updated_at\": \"2020-05-04T03:18:24.818629Z\"\n }\n}"
}
]
},
{
"name": "Incident Updates",
"event": [
{
"listen": "test",
"script": {
"id": "e2e7d38f-efae-44d1-9361-cd301547feb3",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/2/updates",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"2",
"updates"
]
},
"description": "View all incidents for a single service."
},
"response": []
},
{
"name": "Create Incident Update",
"event": [
@ -2609,6 +2564,113 @@
}
]
},
{
"name": "Incident Updates",
"event": [
{
"listen": "test",
"script": {
"id": "e2e7d38f-efae-44d1-9361-cd301547feb3",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/2/updates",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"2",
"updates"
]
},
"description": "View all incidents for a single service."
},
"response": []
},
{
"name": "Update Service Incident",
"event": [
{
"listen": "test",
"script": {
"id": "42cd68de-5bd4-4d4e-b687-fa0bafc1d61a",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"title\": \"Updated Downtime\",\n\t\"description\": \"This is an update for an incident\"\n}"
},
"url": {
"raw": "{{endpoint}}/api/incidents/1",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"1"
]
},
"description": "View all incidents for a single service."
},
"response": []
},
{
"name": "Delete Incident Update",
"event": [
@ -2712,6 +2774,106 @@
"body": "{\n \"status\": \"success\",\n \"type\": \"incident_update\",\n \"method\": \"delete\",\n \"id\": 1,\n \"output\": {\n \"id\": 1,\n \"message\": \"Website is loading very slowly, looking into this.\",\n \"type\": \"Investigating\",\n \"created_at\": \"2020-05-04T03:20:05.102435Z\",\n \"updated_at\": \"2020-05-04T03:20:05.102435Z\"\n }\n}"
}
]
},
{
"name": "Delete Incident",
"event": [
{
"listen": "test",
"script": {
"id": "c6735dbe-86e8-4b42-9b04-6fc1fea949df",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/1",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"1"
]
},
"description": "View all incidents for a single service."
},
"response": [
{
"name": "Delete Incident",
"originalRequest": {
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"url": {
"raw": "{{endpoint}}/api/incidents/2",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"incidents",
"2"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Date",
"value": "Mon, 04 May 2020 03:31:48 GMT"
},
{
"key": "Content-Length",
"value": "207"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"status\": \"success\",\n \"type\": \"incident\",\n \"method\": \"delete\",\n \"id\": 2,\n \"output\": {\n \"id\": 2,\n \"title\": \"Service Downtime\",\n \"service\": 1,\n \"created_at\": \"2020-05-04T03:18:24.818629Z\",\n \"updated_at\": \"2020-05-04T03:18:24.818629Z\"\n }\n}"
}
]
}
],
"protocolProfileBehavior": {}
@ -3212,6 +3374,8 @@
"pm.test(\"Check Login JWT Token\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('token');",
" pm.expect(jsonData).to.have.property('admin');",
" pm.globals.set(\"token\", jsonData.token);",
"});"
],
"type": "text/javascript"
@ -3308,24 +3472,141 @@
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Length",
"value": "174"
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI; Path=/; Expires=Thu, 06 Aug 2020 19:44:13 GMT; Max-Age=259200"
},
{
"key": "Date",
"value": "Mon, 03 Aug 2020 19:44:13 GMT"
},
{
"key": "Content-Length",
"value": "197"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI\",\n \"admin\": true\n}"
}
]
},
{
"name": "Check User Token",
"event": [
{
"listen": "test",
"script": {
"id": "560e439b-d588-4a2f-a8a6-a0607531d74c",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"View Token Response\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.username).to.eql(\"admin\");",
" pm.expect(jsonData.admin).to.eql(true);",
" pm.expect(jsonData.scopes).to.eql(\"admin\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
},
"description": "Send your JWT token from login to this endpoint to return the JSON values."
},
"response": [
{
"name": "Check User Token",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Date",
"value": "Sat, 02 May 2020 00:56:17 GMT"
"value": "Mon, 03 Aug 2020 19:47:23 GMT"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY; Expires=Tue, 05 May 2020 00:56:17 GMT"
"key": "Content-Length",
"value": "68"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY\",\n \"admin\": true\n}"
"body": "{\n \"username\": \"admin\",\n \"admin\": true,\n \"scopes\": \"admin\",\n \"exp\": 1596743053\n}"
}
]
},
@ -3917,7 +4198,7 @@
"",
"pm.test(\"View All Notifiers\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.length).to.eql(11);",
" pm.expect(jsonData.length).to.eql(13);",
"});"
],
"type": "text/javascript"
@ -4121,7 +4402,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"method\": \"slack\",\n \"host\": \"https://hooks.slack.com/services/EXAMPLEIDHERE/BV33WKP0C/MtKw3Kc8BFylTv4pohKqHtXX\",\n \"enabled\": true,\n \"limits\": 55\n}",
"raw": "{\n \"method\": \"slack\",\n \"host\": \"https://hooks.slack.com/services/TTJ1B90DP/RENU20O9M/9uI823SUnYBuGcxYlpSimD6H\",\n \"enabled\": true,\n \"limits\": 55\n}",
"options": {
"raw": {}
}
@ -4233,7 +4514,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"method\": \"success\",\n \"notifier\": {\n \"enabled\": false,\n \"limits\": 60,\n \"method\": \"slack\",\n \"host\": \"https://webhooksurl.slack.com/***\",\n \"success_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"The service {{.Service.Name}} is back online.\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"primary\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\",\n \"failure_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\":warning: The service {{.Service.Name}} is currently offline! :warning:\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"section\\\",\\n \\\"fields\\\": [{\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Service:*\\\\n{{.Service.Name}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*URL:*\\\\n{{.Service.Domain}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Status Code:*\\\\n{{.Service.LastStatusCode}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*When:*\\\\n{{.Failure.CreatedAt}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Downtime:*\\\\n{{.Service.DowntimeAgo}}\\\"\\n }, {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"*Error:*\\\\n{{.Failure.Issue}}\\\"\\n }]\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Offline Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"danger\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\"\n }\n}",
"raw": "{\n \"method\": \"success\",\n \"notifier\": {\n \"enabled\": false,\n \"limits\": 60,\n \"method\": \"slack\",\n \"host\": \"https://webhooksurl.slack.com/***\",\n \"success_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"The service {{.Service.Name}} is back online.\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"primary\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\",\n \"failure_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\":warning: The service {{.Service.Name}} is currently offline! :warning:\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"section\\\",\\n \\\"fields\\\": [{\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Service:*\\\\n{{.Service.Name}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*URL:*\\\\n{{.Service.Domain}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Status Code:*\\\\n{{.Service.LastStatusCode}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*When:*\\\\n{{.Failure.CreatedAt}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Downtime:*\\\\n{{.Service.Downtime.Human}}\\\"\\n }, {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"*Error:*\\\\n{{.Failure.Issue}}\\\"\\n }]\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Offline Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"danger\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\"\n }\n}",
"options": {
"raw": {}
}
@ -4911,8 +5192,7 @@
" var first = jsonData[0];",
" var id = pm.globals.get(\"checkin_id\");",
" pm.expect(first.name).to.eql(\"Demo Checkin 1\");",
" pm.expect(first.grace).to.eql(300);",
" pm.expect(first.interval).to.eql(300);",
" pm.expect(first.interval).to.eql(3);",
"});"
],
"type": "text/javascript"
@ -4990,8 +5270,7 @@
" pm.expect(jsonData.status).to.eql(\"success\");",
" pm.expect(jsonData.type).to.eql(\"checkin\");",
" pm.expect(jsonData.output.name).to.eql(\"Server Checkin\");",
" pm.expect(jsonData.output.grace).to.eql(60);",
" pm.expect(jsonData.output.interval).to.eql(900);",
" pm.expect(jsonData.output.interval).to.eql(3);",
" var id = jsonData.output.api_key;",
" pm.globals.set(\"checkin_id\", id);",
"});"
@ -5022,7 +5301,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"service_id\": 2,\n \"name\": \"Server Checkin\",\n \"interval\": 900,\n \"grace\": 60\n}",
"raw": "{\n \"service_id\": 2,\n \"name\": \"Server Checkin\",\n \"interval\": 3\n}",
"options": {
"raw": {}
}
@ -5182,8 +5461,7 @@
" var id = pm.globals.get(\"checkin_id\");",
" pm.expect(jsonData.name).to.eql(\"Server Checkin\");",
" pm.expect(jsonData.api_key).to.eql(id);",
" pm.expect(jsonData.grace).to.eql(60);",
" pm.expect(jsonData.interval).to.eql(900);",
" pm.expect(jsonData.interval).to.eql(3);",
"});"
],
"type": "text/javascript"

View File

@ -60,6 +60,9 @@ const webpackConfig = merge(commonConfig, {
plugins: [
new webpack.EnvironmentPlugin(environment),
new CleanWebpackPlugin(),
// new webpack.optimize.LimitChunkCountPlugin({
// maxChunks: 1
// }),
new MiniCSSExtractPlugin({
filename: 'css/[name].css',
chunkFilename: 'css/[name].css'
@ -71,7 +74,7 @@ const webpackConfig = merge(commonConfig, {
threshold: 10240,
minRatio: 0.8
}),
new webpack.HashedModuleIdsPlugin(),
// new webpack.HashedModuleIdsPlugin(),
new HtmlPlugin({
template: 'public/base.gohtml',
filename: 'base.gohtml',

View File

@ -19,26 +19,25 @@
"@fortawesome/free-brands-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@sentry/browser": "^5.13.2",
"@sentry/integrations": "^5.13.2",
"apexcharts": "^3.15.0",
"@sentry/browser": "^5.20.1",
"@sentry/integrations": "^5.20.1",
"apexcharts": "^3.6.6",
"axios": "^0.19.1",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.6.1",
"codemirror-colorpicker": "^1.9.66",
"core-js": "^3.4.4",
"core-js": "^3.6.5",
"date-fns": "^2.9.0",
"js-beautify": "^1.11.0",
"querystring": "^0.2.0",
"sass": "^1.26.10",
"semver": "^7.3.2",
"vue": "^2.6.11",
"vue-apexcharts": "^1.5.2",
"vue-apexcharts": "^1.6.0",
"vue-clipboard2": "^0.3.1",
"vue-codemirror": "^4.0.6",
"vue-cookies": "^1.7.0",
"vue-flatpickr-component": "^8.1.5",
"vue-github-button": "^1.1.2",
"vue-i18n": "^8.18.1",
"vue-moment": "^4.1.0",
"vue-observe-visibility": "^0.4.6",
"vue-router": "~3.0",
"vuedraggable": "^2.23.2",
@ -64,7 +63,6 @@
"compression-webpack-plugin": "~2.0",
"cross-env": "^7.0.2",
"css-loader": "~2.1",
"cypress": "^4.3.0",
"eslint": "~5.16",
"eslint-config-standard": "~10.2",
"eslint-friendly-formatter": "~3.0",
@ -78,13 +76,13 @@
"expect": "^25.1.0",
"file-loader": "^5.0.2",
"friendly-errors-webpack-plugin": "~1.7",
"github-wikito-converter": "^1.5.2",
"html-webpack-plugin": "^4.0.0-beta.11",
"jsdom": "^16.2.0",
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "~0.5",
"mocha": "^7.0.1",
"mochapack": "^1.1.13",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "~5.0",
"sass-loader": "^8.0.2",
"start-server-and-test": "^1.10.11",

View File

@ -43,19 +43,11 @@
<meta property="twitter:image" content="favicon/social.png">
{{if USE_CDN}}
<link rel="stylesheet" href="https://assets.statping.com/vendor.css">
<link rel="stylesheet" href="https://assets.statping.com/style.css">
<link rel="stylesheet" href="https://assets.statping.com/main.css">
{{else}}
{{if USING_ASSETS}}
<link href="css/vendor.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
<link rel="stylesheet" href="https://assets.statping.com/css/vendor.css">
{{else}}
<% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %>
<%= headTag %> <% }) %>
{{end}}
{{end}}
</head>
<body>
<noscript>
@ -65,11 +57,10 @@
<div id="app" class="statping_container"></div>
{{if USE_CDN}}
<script src="https://assets.statping.com/bundle.js"></script>
<script src="https://assets.statping.com/vendor.chunk.js"></script>
<script src="https://assets.statping.com/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/style.chunk.js"></script>
<script src="https://assets.statping.com/main.chunk.js"></script>
<script src="https://assets.statping.com/js/bundle.js"></script>
<script src="https://assets.statping.com/js/vendor.chunk.js"></script>
<script src="https://assets.statping.com/js/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/js/main.chunk.js"></script>
{{else}}
<% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %>
<%= bodyTag %> <% }) %>

View File

@ -113,10 +113,6 @@ HTML, BODY {
color: #fff;
}
.nav-pills .nav-link {
border-radius: 0.2rem;
}
.form-control {
border-radius: 0.2rem;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,16 +1,14 @@
import Vue from "vue";
import axios from 'axios'
import * as Sentry from "@sentry/browser";
import * as Integrations from "@sentry/integrations";
const qs = require('querystring');
axios.defaults.withCredentials = true
const tokenKey = "statping_auth";
const errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
class Api {
constructor() {
this.version = "0.90.67";
this.commit = "7e121335791d2143a2eefd404dbcce83b8f46f61";
}
async oauth() {
@ -54,17 +52,17 @@ class Api {
return axios.post('api/services/' + data.id, data).then(response => (response.data))
}
async service_hits(id, start, end, group, fill=true) {
async service_hits(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_ping(id, start, end, group, fill=true) {
return axios.get('api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_ping(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_failures_data(id, start, end, group, fill=true) {
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_failures_data(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_uptime(id, start, end) {
return axios.get('api/services/' + id + '/uptime_data?start=' + start + '&end=' + end).then(response => (response.data))
@ -75,7 +73,7 @@ class Api {
}
async service_failures(id, start, end, limit = 999, offset = 0) {
return axios.get('api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit+ '&offset=' + offset).then(response => (response.data))
return axios.get('api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit + '&offset=' + offset).then(response => (response.data))
}
async service_failures_delete(service) {
@ -90,16 +88,15 @@ class Api {
return axios.post('api/reorder/services', data).then(response => (response.data))
}
async checkins() {
return axios.get('api/checkins').then(response => (response.data))
}
async checkins() {
return axios.get('api/checkins').then(response => (response.data))
}
async groups() {
return axios.get('api/groups').then(response => (response.data))
}
async groups_reorder(data) {
window.console.log('api/reorder/groups', data)
return axios.post('api/reorder/groups', data).then(response => (response.data))
}
@ -132,36 +129,40 @@ class Api {
}
async incident_updates(incident) {
return axios.get('api/incidents/'+incident.id+'/updates').then(response => (response.data))
return axios.get('api/incidents/' + incident.id + '/updates').then(response => (response.data))
}
async incident_update_create(update) {
return axios.post('api/incidents/'+update.incident+'/updates', update).then(response => (response.data))
return axios.post('api/incidents/' + update.incident + '/updates', update).then(response => (response.data))
}
async incident_update_delete(update) {
return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data))
return axios.delete('api/incidents/' + update.incident + '/updates/' + update.id).then(response => (response.data))
}
async incidents_service(id) {
return axios.get('api/services/'+id+'/incidents').then(response => (response.data))
}
async incidents_service(id) {
return axios.get('api/services/' + id + '/incidents').then(response => (response.data))
}
async incident_create(service_id, data) {
return axios.post('api/services/'+service_id+'/incidents', data).then(response => (response.data))
}
async incident_create(service_id, data) {
return axios.post('api/services/' + service_id + '/incidents', data).then(response => (response.data))
}
async incident_delete(incident) {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data))
}
async incident_delete(incident) {
return axios.delete('api/incidents/' + incident.id).then(response => (response.data))
}
async checkin_create(data) {
return axios.post('api/checkins', data).then(response => (response.data))
}
async checkin(api) {
return axios.get('api/checkins/' + api).then(response => (response.data))
}
async checkin_delete(checkin) {
return axios.delete('api/checkins/'+checkin.api_key).then(response => (response.data))
}
async checkin_create(data) {
return axios.post('api/checkins', data).then(response => (response.data))
}
async checkin_delete(checkin) {
return axios.delete('api/checkins/' + checkin.api_key).then(response => (response.data))
}
async messages() {
return axios.get('api/messages').then(response => (response.data))
@ -231,6 +232,15 @@ class Api {
return axios.post('api/theme', data).then(response => (response.data))
}
async import(data) {
return axios.post('api/settings/import', data).then(response => (response.data))
}
async check_token(token) {
const f = {token: token}
return axios.post('api/users/token', qs.stringify(f)).then(response => (response.data))
}
async login(username, password) {
const f = {username: username, password: password}
return axios.post('api/login', qs.stringify(f)).then(response => (response.data))
@ -251,6 +261,14 @@ class Api {
})
}
async configs() {
return axios.get('api/settings/configs').then(response => (response.data)) || []
}
async configs_save(data) {
return axios.post('api/settings/configs', data).then(response => (response.data)) || []
}
token() {
return $cookies.get(tokenKey);
}
@ -264,15 +282,12 @@ class Api {
}
}
async allActions(...all) {
await axios.all([all])
async github_release() {
return fetch('https://api.github.com/repos/statping/statping/releases/latest').then(response => response.json())
}
async sentry_init() {
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
async allActions(...all) {
await axios.all([all])
}
}

View File

@ -1,59 +1,59 @@
<template>
<div id="app">
<router-view :loaded="loaded"/>
<Footer v-if="$route.path !== '/setup'"/>
<router-view/>
<Footer v-if="$route.path !== '/setup'"/>
</div>
</template>
<script>
import Footer from "./components/Index/Footer";
const Footer = () => import(/* webpackChunkName: "index" */ "./components/Index/Footer");
export default {
name: 'app',
components: {
Footer
},
data () {
return {
loaded: false,
name: 'app',
components: {
Footer
},
data() {
return {
loaded: false,
version: "",
}
},
computed: {
core() {
return this.$store.getters.core
}
},
async beforeMount() {
await this.$store.dispatch('loadCore')
}
},
computed: {
core() {
return this.$store.getters.core
}
},
async beforeMount() {
await this.$store.dispatch('loadCore')
this.$i18n.locale = this.core.language || "en";
// this.$i18n.locale = "ru";
this.$i18n.locale = this.core.language || "en";
// this.$i18n.locale = "ru";
if (!this.core.setup) {
this.$router.push('/setup')
}
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
await this.$store.dispatch('loadAdmin')
} else {
await this.$store.dispatch('loadRequired')
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
await this.$store.dispatch('loadAdmin')
} else {
await this.$store.dispatch('loadRequired')
}
this.loaded = true
}
this.loaded = true
}
},
},
async mounted() {
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
this.logged_in = true
// await this.$store.dispatch('loadAdmin')
}
}
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
this.logged_in = true
// await this.$store.dispatch('loadAdmin')
}
}
}
}
}
</script>
<style lang="scss">
@import "./assets/css/bootstrap.min.css";
@import "./assets/scss/main";
@import "./assets/scss/index";
</style>

View File

@ -1,8 +1,5 @@
@import 'variables';
HTML,BODY {
background-color: $background-color;
}
@import 'mixin';
.index-chart {
height: $service-card-height;
@ -26,26 +23,11 @@ HTML,BODY {
box-shadow: 0px 3px 6px 1px rgba(0,0,0,0.08);
}
.copy-btn {
position: absolute;
right: 0;
}
.btn-xs {
font-size: 8pt;
padding: 2px 6px;
}
.copy-btn BUTTON {
background-color: white;
margin: 6px;
height: 26px;
font-size: 8pt;
padding: 5px 7px;
border: 1px solid #a7a7a7;
border-radius: 4px !important;
}
.dim {
background-color: #f3f3f3;
}
@ -55,41 +37,6 @@ HTML,BODY {
font-weight: bold;
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 5px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
border-radius: 50%;
width: 20px; /* Set a specific slider handle width */
height: 20px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 15px; /* Set a specific slider handle width */
height: 15px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
@-o-keyframes fadeIt {
0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; }
@ -136,43 +83,53 @@ HTML,BODY {
}
.chartmarker {
padding: 5px;
width: 240px;
text-align: right;
padding: 0px;
width: 200px;
text-align: left;
}
.chartmarker SPAN {
font-size: 9pt;
font-size: 4pt;
display: block;
color: #8b8b8b;
color: #b1b1b1;
}
.apexcharts-tooltip {
box-shadow: none;
}
.contain-card {
.card-header {
font-size: 1.15rem;
}
.dropdown-menu {
background-color: rgba(239, 239, 239, 0.65);
}
}
.dropup .dropdown-menu {
border-radius: 8px 8px 8px 0;
background-color: #efefef;
}
.divided {
display: flex;
align-items: center;
margin-bottom: 0;
}
.divider {
flex-grow: 1;
border-bottom: 1px solid rgba(0,0,0,0.1);
margin: 0 20px 0 20px;
}
.daily-failures {
position: absolute;
padding-top: 3px;
top: 10px;
right: 100px;
width: 300px;
height: 25px;
}
.service_day {
height: 20px;
margin-right: 2px;
border-radius: 4px;
max-width: 25px;
border-radius: 2px;
max-width: 30px;
cursor: pointer;
}
.service_day SPAN {
@ -202,13 +159,6 @@ HTML,BODY {
opacity: 0;
}
.container {
padding-top: 20px;
padding-bottom: 25px;
max-width: $max-width;
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.header-title {
color: $title-color;
}
@ -226,10 +176,6 @@ HTML,BODY {
padding: 5px 7px;
}
.navbar {
margin-bottom: 30px;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;
@ -274,20 +220,6 @@ HTML,BODY {
color: $service-description-color
}
.footer {
text-decoration: none;
margin-top: 20px;
}
.footer A {
color: $footer-text-color;
text-decoration: none;
}
.footer A:HOVER {
color: #6d6d6d;
}
.font-0 {
font-size: 0.35rem;
}
@ -342,22 +274,6 @@ HTML,BODY {
}
}
.card-body .badge {
color: #fff;
}
.nav-pills .nav-link {
border-radius: $global-border-radius;
}
.form-control {
border-radius: $global-border-radius;
}
.mini_success {
background-color: #f3f3f3;
}
.no-decoration {
color: black;
text-decoration: none;
@ -368,10 +284,6 @@ HTML,BODY {
text-decoration: none;
}
.mini_error {
background-color: #ffbbbb;
}
.btn-white {
background-color: white;
border: 1px solid #d8d8d8;
@ -395,10 +307,6 @@ HTML,BODY {
color: #a0a0a0;
}
.service_block {
min-height: 340px;
}
.json-field {
font-size: 10pt;
}
@ -418,27 +326,6 @@ HTML,BODY {
font-weight: bold;
}
.card {
background-color: $service-background;
border: $service-border;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
}
.card-body {
overflow: hidden;
}
.card-body H4 A {
color: $service-title;
text-decoration: none;
}
.card-title A {
color: $service-title;
text-decoration: none;
}
.chart-container {
position: relative;
height: 240px;
@ -458,79 +345,6 @@ HTML,BODY {
width: 100%;
}
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem;
}
input.inputTags-field:focus {
outline-width: 0;
}
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px;
}
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer;
}
@mixin dynamic-color-hov($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-dark:HOVER {
background-color: darken($color, 17%) !important;
border-color: darken($color, 20%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
&.dyn-light:HOVER {
background-color: lighten($color, 17%) !important;
border-color: lighten($color, 20%) !important;
}
}
@mixin dynamic-color($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
}
.btn-primary {
background-color: $primary-color;
border-color: darken($primary-color, 17%);
@ -564,18 +378,6 @@ HTML,BODY {
background-color: darken($danger-color, 10%) !important;
}
.nav-pills .nav-link.active, .nav-pills .show>.nav-link {
background-color: $nav-tab-color;
}
.nav-pills A {
color: #424242;
}
.nav-pills I {
margin-right: 10px;
}
@keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
@ -600,181 +402,13 @@ HTML,BODY {
-webkit-animation: fadeInOut 1s infinite;
-moz-animation: fadeInOut 1s infinite;
-o-animation: fadeInOut 1s infinite;
animation: fadeInOut 21 infinite;
}
.CodeMirror {
/* Bootstrap Settings */
box-sizing: border-box;
margin: 0;
font: inherit;
overflow: auto;
display: block;
width: 100%;
padding: 0px;
font-size: 14px;
line-height: 1.5;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
/* Code Mirror Settings */
font-family: monospace;
position: relative;
height:80vh;
}
.CodeMirror-focused {
/* Bootstrap Settings */
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.switch {
font-size: 1rem;
position: relative;
}
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block;
}
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all;
}
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all;
}
.switch-rd-gr input:checked + label::before {
background-color: #29b10c !important;
}
.switch input:checked + label::before {
background-color: #08d;
}
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed;
}
.switch input:disabled + label::before {
background-color: #e9ecef;
}
.switch.switch-sm {
font-size: 0.875rem;
}
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg {
font-size: 1.25rem;
}
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8);
}
.switch + .switch {
margin-left: 1rem;
animation: fadeInOut 1s infinite;
}
.sortable_drag {
background-color: #0000000f;
}
.drag_icon {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
width: 25px;
height: 25px;
display: inline-block;
margin-right: 5px;
margin-left: -10px;
text-align: center;
color: #b1b1b1;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */
.drag_icon:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
.switch_btn {
float: right;
margin: -1px 0px 0px 0px;
@ -799,7 +433,7 @@ HTML,BODY {
}
.jumbotron {
background-color: white;
background-color: rgba(0,0,0,0);
}
.toggle-service {
@ -809,19 +443,6 @@ HTML,BODY {
cursor: pointer;
}
.list-group-item {
min-height: 85pt;
}
.list-group-item:HOVER {
background-color: #fff;
}
.index_container {
min-height: 980pt;
background-color: $container-color;
}
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {

View File

@ -0,0 +1,299 @@
@import 'variables';
@import 'mixin';
.copy-btn {
position: absolute;
right: 0;
z-index: 995;
}
.copy-btn BUTTON {
background-color: white;
margin: 6px;
height: 26px;
font-size: 8pt;
padding: 5px 7px;
border: 1px solid #a7a7a7;
border-radius: 4px !important;
}
.form-control {
background-color: $input-background;
border: $input-border;
color: $input-color;
}
.form-control:FOCUS {
background-color: lighten($input-background, 4%) !important;
border: $input-border;
color: $input-color;
}
.form-control[readonly] {
background-color: lighten($background-color, 12%) !important;
color: lighten($input-color, 30%) !important;
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 5px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
border-radius: 50%;
width: 20px; /* Set a specific slider handle width */
height: 20px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 15px; /* Set a specific slider handle width */
height: 15px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem;
}
input.inputTags-field:focus {
outline-width: 0;
}
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px;
}
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer;
}
.switch {
font-size: 1rem;
position: relative;
}
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block;
}
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all;
}
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all;
}
.switch-rd-gr input:checked + label::before {
background-color: #29b10c !important;
}
.switch input:checked + label::before {
background-color: #08d;
}
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed;
}
.switch input:disabled + label::before {
background-color: #e9ecef;
}
.switch.switch-sm {
font-size: 0.875rem;
}
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg {
font-size: 1.25rem;
}
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8);
}
.switch + .switch {
margin-left: 1rem;
}
.CodeMirror {
/* Bootstrap Settings */
box-sizing: border-box;
margin: 0;
font: inherit;
overflow: auto;
display: block;
width: 100%;
padding: 0px;
font-size: 14px;
line-height: 1.5;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
/* Code Mirror Settings */
font-family: monospace;
position: relative;
height:80vh;
}
.CodeMirror-focused {
/* Bootstrap Settings */
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.nav-pills .nav-link.active, .nav-pills .show>.nav-link {
background-color: $nav-tab-color;
}
.nav-pills A {
color: $text-color;
}
.nav-link.active A:HOVER {
color: white !important;
}
.nav-pills I {
margin-right: 10px;
}
.drag_icon {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
width: 25px;
height: 25px;
display: inline-block;
margin-right: 5px;
margin-left: -10px;
text-align: center;
color: #b1b1b1;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */
.drag_icon:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}

View File

@ -0,0 +1,6 @@
@import 'variables';
@import 'mixin';
@import 'layout';
@import 'base';
@import 'forms';
@import 'mobile';

View File

@ -0,0 +1,230 @@
@import 'variables';
@import 'mixin';
HTML,BODY {
background-color: $background-color;
color: $text-color;
}
A {
color: $text-color;
}
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
z-index: 1040;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal {
z-index: 999999 !important;
display: block;
}
.modal-dialog {
top: 20%;
}
.modal-header {
padding: 0.5rem 1rem;
}
.modal-footer {
padding: 0.5rem 1rem;
}
.text-muted {
color: lighten($text-color, 30%) !important;
}
.day-success {
background-color: $day-success-background;
}
.day-success:HOVER {
background-color: lighten($day-success-background, 2%) !important;
}
.day-error {
background-color: $day-error-background;
}
.day-error:HOVER {
background-color: lighten($day-error-background, 2%) !important;
}
.contain-card {
.card-header {
font-size: 1.15rem;
}
.dropdown-menu {
background-color: rgba(239, 239, 239, 0.65);
}
}
.navbar {
margin-bottom: 30px;
color: $navbar-color;
background-color: $navbar-background;
}
.table {
color: $text-color;
}
.nav-pills {
border-radius: $global-border-radius;
}
.nav-link {
color: $navbar-color;
}
.form-control {
border-radius: $global-border-radius;
}
.card {
background-color: $card-background;
border: $card-border;
box-shadow: $card-shadow;
}
.card-body {
overflow: hidden;
}
.card-body H4 A {
color: $service-title;
text-decoration: none;
}
.card-title A {
color: $service-title;
text-decoration: none;
}
.card-body .badge {
color: #fff;
}
.dashboard_card {
background-color: $group-list-background;
box-shadow: rgba(0,0,0,.05) 0px 2px 3px 1px;
}
.dashboard_card:HOVER {
background-color: lighten($group-list-background, 2%) !important;
box-shadow: rgba(0,0,0,.05) 0px 1px 5px 3px;
-webkit-transition-duration: 300ms;
-moz-transition-duration: 300ms;
-o-transition-duration: 300ms;
transition-duration: 300ms;
}
.list-group-item {
min-height: 85pt;
background-color: $group-list-background;
}
.list-group-item:HOVER {
background-color: lighten($group-list-background, 5%) !important;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px 1px;
-webkit-transition-duration: 300ms;
-moz-transition-duration: 300ms;
-o-transition-duration: 300ms;
transition-duration: 300ms;
margin-top: -1px;
margin-bottom: 1px;
}
.list-group-item A {
color: $group-list-title;
}
.chart_list_tooltip {
height: 30px;
font-size: 6pt;
}
.container {
padding-top: 20px;
padding-bottom: 25px;
max-width: $max-width;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15) !important;
background-color: $container-color;
}
.login_container {
border-radius: 5px;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15) !important;
background-color: $container-color;
}
.footer {
text-decoration: none;
margin-top: 20px;
}
.footer .links {
color: $footer-text-color;
text-decoration: none;
}
.footer .links:HOVER {
color: #8c8c8c;
}
.footer .statping {
color: lighten($footer-text-color, 10%);
text-decoration: none;
}
.footer .statping:HOVER {
color: lighten($footer-text-color, 0%);
text-decoration: none;
}
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.dates{
padding:10px 0px;
margin:10px 20px;
font-weight:600;
overflow:auto;
color: darken($card-background, 45%);
font-size: 22px;
}
.dates div{
float:left;
width:50%;
text-align:center;
position:relative;
}
.dates strong {
display:block;
color: darken($card-background, 20%);
font-size:20px;
font-weight:700;
}
.dates span{
width:1px;
height:60px;
position:absolute;
right:0;
top:0;
background: darken($card-background, 10%);
}

View File

@ -1,2 +0,0 @@
@import 'base';
@import 'mobile';

View File

@ -0,0 +1,29 @@
@mixin dynamic-color-hov($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-dark:HOVER {
background-color: darken($color, 17%) !important;
border-color: darken($color, 20%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
&.dyn-light:HOVER {
background-color: lighten($color, 17%) !important;
border-color: lighten($color, 20%) !important;
}
}
@mixin dynamic-color($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
border-color: darken($color, 17%) !important;
}
&.dyn-light {
background-color: lighten($color, 12%) !important;
border-color: lighten($color, 17%) !important;
}
}

View File

@ -40,10 +40,6 @@
font-size: 0.9rem;
}
.service_li {
border: 1px solid #f3f3f3 !important;
}
.container {
padding: 0px !important;
padding-top: 0vh !important;
@ -74,10 +70,19 @@
position: absolute;
}
.btn-sm {
line-height: 1.4rem;
font-size: 0.83rem;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;
}
.badge {
padding-top: 2px;
padding-bottom: 3px;
}
.switch LABEL {
font-size: 10pt;
}
.full-col-12 {
padding-left: 0px;

View File

@ -1,16 +1,28 @@
/* Index Page */
$background-color: #fcfcfc;
$container-color: #fcfcfc;
$max-width: 860px;
$title-color: #464646;
$description-color: #939393;
$background-color: #EAEAEA;
$container-color: #ffffff;
$text-color: #2a2a2a;
$max-width: 1012px;
$title-color: #4e4e4e;
$description-color: #828282;
$subtitle-color: #747474;
$mobile-card-shadow: 2px 3px 10px #b7b7b7;
$group-list-background: #fcfcfc;
$group-list-title: #474747;
$navbar-color: #1c1c1c;
$navbar-background: #ffffff;
$input-background: #fdfdfd;
$input-color: #4e4e4e;
$input-border: 1px solid #c9c9c9;
$day-success-background: #e9e9e9;
$day-error-background: #d50a0a;
/* Status Container */
$service-background: #ffffff;
$service-border: 1px solid rgba(0,0,0,.125);
$service-title: #444444;
$card-background: #fcfcfc;
$card-border: 1px solid rgba(76, 76, 76, 0.12);
$card-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
$service-title: #3e3e3e;
$service-title-size: 1.8rem;
$service-stats-color: #4f4f4f;
$service-description-color: #fff;
@ -23,7 +35,7 @@ $danger-color: #dd3545;
$primary-color: #3e9bff;
/* Footer Settings */
$footer-text-color: #8d8d8d;
$footer-text-color: #b0b0b0;
$nav-tab-color: #13a00d;
$footer-display: block;

View File

@ -6,7 +6,7 @@ CodeMirror.defineMode('mymode', () => {
if (stream.match(".Service") || (stream.match(".Core")) || (stream.match(".Failure"))) {
return "var-highlight"
} else if (stream.match(".Id") || stream.match(".Domain") || stream.match(".CreatedAt") ||
stream.match(".Name") || stream.match(".DowntimeAgo") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Name") || stream.match(".Downtime.Human") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Port") || stream.match(".FailuresLast24Hours") || stream.match(".PingTime")) {
return "var-sub-highlight"
} else if (stream.match("{{") || stream.match("}}")) {

View File

@ -1,5 +1,5 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card mb-5">
<div class="card-header">Cache</div>
<div class="card-body">
<span v-if="!cache" class="text-muted">There are no cached pages yet!</span>

View File

@ -2,40 +2,80 @@
<div class="col-12">
<h2>{{service.name}} Checkins</h2>
<p class="mb-3">Tell your service to send a routine HTTP request to a Statping Checkin.</p>
<div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
<span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
<span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
<span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
<span class="d-block mt-2">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
<button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
<div v-for="(checkin, i) in checkins" class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
{{checkin.name}}
<button @click="deleteCheckin(checkin)" class="btn btn-sm small btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/checkin/${checkin.api_key}`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small">Send a GET request to this URL every {{checkin.interval}} minutes</span>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Request expected every {{checkin.interval}} minutes</span>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{checkin.name}} Records
</div>
<div class="card-body" :class="{'d-none': !expanded}">
<div class="alert alert-primary small" :class="{'alert-success': hit.success, 'alert-danger': !hit.success}" v-for="(hit, i) in records(checkin)">
Checkin {{hit.success ? "Request" : "Failure"}} at {{hit.created_at}}
</div>
</div>
</div>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="curl_expanded = !curl_expanded" :icon="curl_expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
Cronjob Task
</div>
<div class="card-body" :class="{'d-none': !curl_expanded}">
This cronjob script will request the checkin endpoint every {{checkin.interval}} minutes. Add this cronjob task to the machine running this service.
<div class="input-group mt-2">
<input type="text" class="form-control" :value="`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small d-block">Using CURL</span>
</div>
</div>
</div>
<div class="card-footer">
<span :class="{'text-success': last_record(checkin).success, 'text-danger': !last_record(checkin).success}">
{{last_record(checkin).success ? "Checkin is currently working correctly" : "Checkin is currently failing"}}
</span>
</span>
</div>
</div>
<div class="col-12 alert alert-light">
<div class="card text-black-50 bg-white mt-4">
<div class="card-header text-capitalize">Create Checkin</div>
<div class="card-body">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-7 col-md-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
<div class="col-5 col-md-3">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<div class="col-12 col-md-4">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
<button :disabled="btn_disabled" @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
@ -49,11 +89,14 @@ export default {
return {
service: {},
ready: false,
expanded: false,
curl_expanded: false,
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: 0
interval: 1,
service_id: 0,
hits: [],
failures: []
}
}
},
@ -64,6 +107,12 @@ export default {
core() {
return this.$store.getters.core
},
btn_disabled() {
if (this.checkin.name === "" || this.checkin.interval <= 0) {
return true
}
return false
},
},
async created() {
if (this.$route.params) {
@ -74,32 +123,43 @@ export default {
}
},
methods: {
records(checkin) {
let hits = []
let failures = []
checkin.hits.forEach((hit) => {
hits.push({success: true, created_at: this.parseISO(hit.created_at), id: hit.id})
})
checkin.failures.forEach((failure) => {
failures.push({success: false, created_at: this.parseISO(failure.created_at), id: failure.id})
})
return hits.concat(failures).sort((a, b) => {return a.created_at-b.created_at}).reverse().slice(0,32)
},
last_record(checkin) {
const r = this.records(checkin)
if (r.length === 0) {
return {success: false}
}
return r[0]
},
fixInts() {
const c = this.checkin
this.checkin.interval = parseInt(c.interval)
this.checkin.grace = parseInt(c.grace)
return this.checkin
},
async saveCheckin() {
const c = this.fixInts()
await Api.checkin_create(c)
await this.updateCheckins()
this.checkin.name = ""
await this.load()
},
async deleteCheckin(checkin) {
await Api.checkin_delete(checkin)
await this.updateCheckins()
await this.load()
},
async updateCheckins() {
async load() {
const checkins = await Api.checkins()
this.$store.commit('setCheckins', checkins)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<h3>Configuration</h3>
For security reasons, all database credentials cannot be editted from this page.
<codemirror v-show="loaded" v-model="configs" ref="configs" :options="cmOptions" class="mt-4 codemirrorInput"/>
<button @click.prevent="save" class="btn col-12 btn-primary mt-3">Save</button>
</div>
</template>
<script>
import Api from "../../API";
import {codemirror} from 'vue-codemirror'
import('codemirror/lib/codemirror.css')
import('codemirror/mode/yaml/yaml.js')
export default {
name: "Configs",
components: {
codemirror
},
data() {
return {
loaded: false,
configs: null,
cmOptions: {
height: 700,
tabSize: 4,
lineNumbers: true,
matchBrackets: true,
mode: "text/x-yaml",
line: true
}
}
},
mounted() {
this.loaded = false
this.update()
this.loaded = true
},
watch: {
"configs" () {
this.$refs.configs.codemirror.refresh()
}
},
methods: {
async update() {
this.configs = await Api.configs()
this.$refs.configs.codemirror.value = this.configs
this.$refs.configs.codemirror.refresh()
},
async save() {
try {
await Api.configs_save(this.configs)
} catch(e) {
window.console.error(e)
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,33 +1,58 @@
<template>
<div class="col-12 mt-4 mt-md-3">
<div class="row stats_area mb-5">
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span>
<span class="font-2">{{ $t('dashboard.total_services') }}</span>
<span class="font-2">{{ $t('total_services') }}</span>
</div>
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{failuresLast24Hours()}}</span>
<span class="font-2">{{ $t('dashboard.failures_24_hours') }}</span>
<span class="font-2">{{ $t('failures_24_hours') }}</span>
</div>
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{$store.getters.onlineServices(true).length}}</span>
<span class="font-2">{{ $t('dashboard.online_services') }}</span>
<span class="font-2">{{ $t('online_services') }}</span>
</div>
</div>
<div v-for="(service, index) in services" class="service_block" v-bind:key="index">
<ServiceInfo :service=service />
<div class="col-12" v-if="services.length === 0">
<div class="alert alert-dark d-block">
{{$t('no_services')}}
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-success float-right">
<font-awesome-icon icon="plus"/> {{ $t('create') }}
</router-link>
</div>
</div>
<div v-for="message in messagesInRange" class="bg-light shadow-sm p-3 pr-4 pl-4 col-12 mb-4">
<font-awesome-icon icon="calendar" class="mr-3" size="1x"/> {{message.description}}
<span class="d-block small text-muted mt-3">
Starts at <strong>{{niceDate(message.start_on)}}</strong> till <strong>{{niceDate(message.end_on)}}</strong>
({{dur(parseISO(message.start_on), parseISO(message.end_on))}})
</span>
</div>
<div class="row">
<div v-for="(service, index) in services_no_group" class="col-12 col-md-4">
<ServiceInfo :service="service" />
</div>
</div>
<div v-for="group in groups">
<GroupedServices :group="group"/>
</div>
</div>
</template>
<script>
const ServiceInfo = () => import('@/components/Service/ServiceInfo')
import GroupedServices from "@/components/Dashboard/GroupedServices";
const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo')
export default {
name: 'DashboardIndex',
components: {
GroupedServices,
ServiceInfo
},
data() {
@ -36,12 +61,20 @@
}
},
computed: {
messagesInRange() {
return this.$store.getters.globalMessages.filter(m => this.isAfter(this.now(), m.start_on) && this.isBefore(this.now(), m.end_on))
},
services() {
return this.$store.getters.services
}
},
services_no_group() {
return this.$store.getters.servicesNoGroup
},
groups() {
return this.$store.getters.groupsInOrder
},
},
methods: {
failuresLast24Hours() {
let total = 0;
this.services.map((s) => {

View File

@ -1,19 +1,26 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{ $t('top_nav.announcements') }}</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('announcements') }}</div>
<div class="card-body pt-0">
<table class="table table-striped">
<div v-if="messages.length === 0">
<div class="alert alert-dark d-block mt-3 mb-0">
You currently don't have any Announcements! Create one using the form below.
</div>
</div>
<table v-else class="table table-striped">
<thead>
<tr>
<th scope="col">{{ $t('dashboard.title') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $tc('dashboard.service', 1) }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('dashboard.begins') }}</th>
<th scope="col">{{ $t('title') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $tc('service', 1) }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('begins') }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="message in $store.getters.messages" v-bind:key="message.id">
<tr v-for="message in messages" v-bind:key="message.id">
<td>{{message.title}}</td>
<td class="d-none d-md-table-cell">
<router-link :to="serviceLink(service(message.service))">{{serviceName(service(message.service))}}</router-link>
@ -40,7 +47,7 @@
<script>
import Api from "../../API"
import FormMessage from "../../forms/Message";
const FormMessage = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Message");
export default {
name: 'DashboardMessages',
@ -51,6 +58,11 @@
message: {}
}
},
computed: {
messages() {
return this.$store.getters.messages
}
},
methods: {
goto(to) {
this.$router.push(to)
@ -69,13 +81,21 @@
serviceName (service) {
return service.name || "Global Message"
},
async delete(m) {
await Api.message_delete(m.id)
const messages = await Api.messages()
this.$store.commit('setMessages', messages)
},
async deleteMessage(m) {
let c = confirm(`Are you sure you want to delete message '${m.title}'?`)
if (c) {
await Api.message_delete(m.id)
const messages = await Api.messages()
this.$store.commit('setMessages', messages)
const modal = {
visible: true,
title: "Delete Announcement",
body: `Are you sure you want to delete Announcement ${m.title}?`,
btnColor: "btn-danger",
btnText: "Delete Announcement",
func: () => this.delete(m),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -1,9 +1,10 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{ $t('top_nav.services') }}
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-outline-success float-right">
<font-awesome-icon icon="plus"/> Create
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('services') }}
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-success float-right">
<font-awesome-icon icon="plus"/> {{$t('create')}}
</router-link>
</div>
<div class="card-body pt-0">
@ -11,15 +12,22 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{ $t('top_nav.groups') }}</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('groups') }}</div>
<div class="card-body pt-0">
<table class="table">
<div v-if="groupsList.length === 0">
<div class="alert alert-dark d-block mt-3 mb-0">
You currently don't have any groups! Create one using the form below.
</div>
</div>
<table v-else class="table">
<thead>
<tr>
<th scope="col">{{ $t('dashboard.name') }}</th>
<th scope="col">{{ $tc('dashboard.service', 2) }}</th>
<th scope="col">{{ $t('dashboard.visibility') }}</th>
<th scope="col">{{ $t('name') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $tc('service', 2) }}</th>
<th scope="col">{{ $t('visibility') }}</th>
<th scope="col"></th>
</tr>
</thead>
@ -29,7 +37,7 @@
<td><span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}}
</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td class="d-none d-md-table-cell">{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
<span class="badge text-uppercase" :class="{'badge-primary': group.public, 'badge-secondary': !group.public}">
{{group.public ? $t('public') : $t('private')}}
@ -60,15 +68,17 @@
</template>
<script>
const FormGroup = () => import('@/forms/Group')
const ToggleSwitch = () => import('@/forms/ToggleSwitch')
const ServicesList = () => import('@/components/Dashboard/ServicesList')
const Modal = () => import(/* webpackChunkName: "dashboard" */ "@/components/Elements/Modal")
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Group')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '@/forms/ToggleSwitch')
const ServicesList = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServicesList')
import Api from "../../API";
import draggable from 'vuedraggable'
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
export default {
name: 'DashboardServices',
components: {
Modal,
ServicesList,
ToggleSwitch,
FormGroup,
@ -105,13 +115,24 @@
this.group = g
this.edit = !mode
},
confirm_delete(service) {
},
async delete(g) {
await Api.group_delete(g.id)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
},
async deleteGroup(g) {
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
if (c) {
await Api.group_delete(g.id)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}
const modal = {
visible: true,
title: "Delete Group",
body: `Are you sure you want to delete group ${g.name}? All services attached will be removed from this group.`,
btnColor: "btn-danger",
btnText: "Delete Group",
func: () => this.delete(g),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{ $t('top_nav.users') }}</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('users') }}</div>
<div class="card-body pt-0">
<table class="table table-striped">
<thead>
@ -9,6 +9,7 @@
<th scope="col">{{$t('username')}}</th>
<th scope="col">{{$t('type')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('last_login') }}</th>
<th scope="col" class="d-none d-md-table-cell">Scopes</th>
<th scope="col"></th>
</tr>
</thead>
@ -21,7 +22,8 @@
{{user.admin ? $t('admin') : $t('user')}}
</span>
</td>
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
<td class="d-none d-md-table-cell">{{user.scopes}}</td>
<td class="text-right">
<div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="#" class="btn btn-outline-secondary edit-user">
@ -45,7 +47,7 @@
<script>
import Api from "../../API"
const FormUser = () => import('@/forms/User')
const FormUser = () => import(/* webpackChunkName: "dashboard" */ '@/forms/User')
export default {
name: 'DashboardUsers',
@ -72,13 +74,21 @@
this.user = u
this.edit = !mode
},
async delete(u) {
await Api.user_delete(u.id)
const users = await Api.users()
this.$store.commit('setUsers', users)
},
async deleteUser(u) {
let c = confirm(`Are you sure you want to delete user '${u.username}'?`)
if (c) {
await Api.user_delete(u.id)
const users = await Api.users()
this.$store.commit('setUsers', users)
const modal = {
visible: true,
title: "Delete User",
body: `Are you sure you want to delete user ${u.username}?`,
btnColor: "btn-danger",
btnText: "Delete User",
func: () => this.delete(u),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -13,11 +13,11 @@
</template>
<script>
import FormGroup from "../../forms/Group";
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Group");
import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable'
import FormService from "../../forms/Service";
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ "../../forms/ToggleSwitch");
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const FormService = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Service");
export default {
name: 'EditService',

View File

@ -1,22 +1,77 @@
<template>
<div class="col-12">
<h2>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-outline-danger float-right">Delete All</button></h2>
<div class="list-group mt-3 mb-4">
<div v-if="service" class="col-12">
<h3>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-danger float-right">Delete All</button>
</h3>
<div class="alert alert-info" v-if="failures.length===0">
You don't have any failures for {{service.name}}. Way to go!
</div>
<div v-for="(failure, index) in failures" :key="index" class="mb-2 list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{failure.issue}}</h5>
<small>{{niceDate(failure.created_at)}}</small>
<div class="card mt-4 mb-4">
<div class="card-header">
Search and Filter
<span class="float-right">
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</span>
</div>
<div class="card-body">
<form>
<div class="form-row">
<div class="col">
<label for="fromdate">From Date</label>
<flatPickr id="fromdate" :disabled="loading" @on-change="load" v-model="start_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" required />
</div>
<div class="col">
<label for="todate">To Date</label>
<flatPickr id="todate" :disabled="loading" @on-change="load" v-model="end_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" required />
</div>
<div class="col">
<label for="search">Search Terms</label>
<input id="search" type="text" v-model="search" class="form-control">
</div>
</div>
<div class="form-row mt-3">
<div class="col">
<span @click="show_checkins = !!show_checkins" class="switch float-left">
<input v-model="show_checkins" type="checkbox" class="switch" id="showcheckins" v-bind:checked="show_checkins">
<label v-if="show_checkins" for="showcheckins">Showing Checkin Failures</label>
<label v-else for="showcheckins">View Checkin Failures</label>
</span>
</div>
</div>
</form>
</div>
<p class="mb-1">{{failure.issue}}</p>
</div>
<nav v-if="total > 4" class="mt-3">
<div v-if="failures.length === 0" class="alert alert-info">
<span v-if="search">
Could not find any failures with issue: "{{search}}"
</span>
<span v-else>
You don't have any failures for {{service.name}}. Way to go!
</span>
</div>
<table v-else class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Issue</th>
<th scope="col">Status Code</th>
<th scope="col">Ping</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
<tr v-for="(failure, index) in failures" :key="index">
<th class="font-1" scope="row">{{failure.id}}</th>
<td class="font-1">{{failure.issue}}</td>
<td class="font-1">{{failure.error_code}}</td>
<td class="font-1">{{humanTime(failure.ping)}}</td>
<td class="font-1">{{ago(failure.created_at)}}</td>
</tr>
</tbody>
</table>
<nav v-if="total > 4 && failures.length !== 0" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{'disabled': page===1}">
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
@ -35,39 +90,59 @@
</li>
</ul>
<div class="text-center">
<span class="text-black-50">{{total}} Total</span>
<span>{{total}} Failures</span>
</div>
</nav>
</div>
</div>
</template>
<script>
import Api from "../../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
export default {
name: 'Failures',
components: {
flatPickr
},
data() {
return {
service: {},
failures: [],
limit: 10,
loading: true,
search: "",
show_checkins: false,
service: null,
fails: [],
limit: 64,
offset: 0,
total: 0,
page: 1
page: 1,
start_time: this.nowSubtract(216000).toISOString(),
end_time: this.nowSubtract(0).toISOString(),
}
},
watch: {
'$route': 'reloadTimes',
},
computed: {
failures() {
let sorted = this.fails
if (this.show_checkins) {
sorted = sorted.filter(f => f.method === "checkin");
} else {
sorted = sorted.filter(f => f.method !== "checkin");
}
if (this.search !== "") {
sorted = sorted.filter(f => f.issue.toLowerCase().includes(this.search));
}
return sorted
},
pages() {
return Math.floor(this.total / this.limit)
},
maxPages() {
const p = Math.floor(this.total / this.limit)
if (p > 16) {
return 16
} else {
return p
}
return Math.floor(this.total / this.limit)
}
},
async created() {
@ -76,14 +151,22 @@ export default {
await this.gotoPage(1)
},
methods: {
async delete() {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
this.total = 0
await this.load()
},
async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?')
if (c) {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
this.total = 0
await this.load()
const modal = {
visible: true,
title: "Delete All Failures",
body: `Are you sure you want to delete all Failures for service ${this.service.title}?`,
btnColor: "btn-danger",
btnText: "Delete Failures",
func: () => this.delete(),
}
this.$store.commit("setModal", modal)
},
async gotoPage(page) {
this.page = page;
@ -91,15 +174,10 @@ export default {
await this.load()
},
async load() {
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
this.loading = true
this.fails = await Api.service_failures(this.service.id, this.toUnix(this.parseISO(this.start_time)), this.toUnix(this.parseISO(this.end_time)), this.limit, this.offset)
this.loading = false
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="row">
<h5 v-if="group.name && group_services" class="h5 col-12 mb-3 mt-2 text-dim">
<font-awesome-icon @click="toggle" :icon="expanded ? 'minus' : 'plus'" class="pointer mr-3"/> {{group.name}}
<span class="badge badge-success text-uppercase float-right ml-2">{{services_online.length}} {{$t('online')}}</span>
<span v-if="services_online.services_offline > 0" class="badge badge-danger text-uppercase float-right">
{{services_offline.length}} {{$t('offline')}}
</span>
</h5>
<div class="col-12 col-md-4" v-if="expanded" v-for="service in group_services">
<ServiceInfo :service="service" />
</div>
</div>
</template>
<script>
const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo')
export default {
name: "GroupedServices",
components: {
ServiceInfo
},
data() {
return {
expanded: true
}
},
props: {
group: {
required: true,
type: Object,
}
},
computed: {
services_online() {
return this.$store.getters.servicesInGroup(this.group.id).filter((s) => s.online)
},
services_offline() {
return this.$store.getters.servicesInGroup(this.group.id).filter((s) => !s.online)
},
group_services() {
return this.$store.getters.servicesInGroup(this.group.id)
},
},
methods: {
toggle() {
this.expanded = !this.expanded
},
dashboard_cookies() {
const data = [{group: 5, show: false}]
if (!this.$cookies.isKey("statping_layout")) {
this.$cookies.set("statping_layout", JSON.stringify(data))
}
}
}
}
</script>

View File

@ -0,0 +1,250 @@
<template>
<div class="mb-5">
<h3 v-if="!loaded" >Import and Export</h3>
<p v-if="!loaded">
You can export your current Statping services, groups, notifiers, and other settings to a JSON file.
</p>
<div v-if="!loaded" class="mt-4 row">
<div class="col-8 custom-file">
<input @change="onFileChange" type="file" class="custom-file-input pointer" id="customFile" accept=".json,application/json">
<label class="custom-file-label" for="customFile">Choose exported Statping JSON file</label>
</div>
<div class="col-4">
<a class="btn btn-block btn-light btn-outline-secondary" href="/api/settings/export">Export</a>
</div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Core Settings
<span @click="file.core.enabled = !!file.core.enabled" class="switch switch-sm float-right">
<input @change="update" v-model="file.core.enabled" type="checkbox" class="switch" :id="`switch-core`">
<label :for="`switch-core`"></label>
</span>
</h3>
<div class="row mb-2"><span class="col-4">Name</span><span class="col-8 text-right font-weight-bold">{{file.core.name}}</span></div>
<div class="row mb-2"><span class="col-4">Description</span><span class="col-8 text-right font-weight-bold">{{file.core.description}}</span></div>
<div class="row mb-2"><span class="col-4">Domain</span><span class="col-8 text-right font-weight-bold">{{file.core.domain}}</span></div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Users
<button @click.prevent="toggle_all(file.users)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.users" class="alert alert-link">
No Users in file
</div>
<div v-for="user in file.users" v-bind:key="user.id" class="row">
<div class="col-4 font-weight-bold">
{{user.username}}
</div>
<div class="col-6">
{{user.email}}
</div>
<div class="col-2 text-right">
<span @click="user.enabled = !!user.enabled" class="switch switch-sm">
<input @change="update" v-model="user.enabled" type="checkbox" class="switch" :id="`switch-user-${user.id}`">
<label :for="`switch-user-${user.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Checkins
<button @click.prevent="toggle_all(file.checkins)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.checkins" class="alert alert-link">
No Checkins in file
</div>
<div v-for="checkin in file.checkins" v-bind:key="checkin.id" class="row">
<div class="col-4 font-weight-bold">
{{checkin.name}}
</div>
<div class="col-6">
Service #{{checkin.service_id}}
</div>
<div class="col-2 text-right">
<span @click="checkin.enabled = !!checkin.enabled" class="switch switch-sm">
<input @change="update" v-model="checkin.enabled" type="checkbox" class="switch" :id="`switch-checkin-${checkin.id}`">
<label :for="`switch-checkin-${checkin.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Services
<button @click.prevent="toggle_all(file.services)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.services" class="alert alert-link">
No Services in file
</div>
<div v-for="service in file.services" v-bind:key="service.id" class="row">
<div class="col-4 font-weight-bold">
{{service.name}}
</div>
<div class="col-6">
{{service.domain}}
</div>
<div class="col-2 text-right">
<span @click="service.enabled = !!service.enabled" class="switch switch-sm">
<input @change="update" v-model="service.enabled" type="checkbox" class="switch" :id="`switch-service-${service.id}`">
<label :for="`switch-service-${service.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Groups
<button @click.prevent="toggle_all(file.groups)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.groups" class="alert alert-link">
No Groups in file
</div>
<div v-for="group in file.groups" v-bind:key="group.id" class="row">
<div class="col-4 font-weight-bold">
{{group.name}}
</div>
<div class="col-8 text-right">
<span @click="group.enabled = !!group.enabled" class="switch switch-sm">
<input @change="update" v-model="group.enabled" type="checkbox" class="switch" :id="`switch-group-${group.id}`">
<label :for="`switch-group-${group.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="loaded" class="col-12 mb-4">
<h3>Incidents
<button @click.prevent="toggle_all(file.incidents)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.incidents" class="alert alert-link">
No Incidents in file
</div>
<div v-for="incident in file.incidents" v-bind:key="incident.id" class="row">
<div class="col-4 font-weight-bold">
{{incident.name}}
</div>
<div class="col-8 text-right">
<span @click="incident.enabled = !!incident.enabled" class="switch switch-sm">
<input @change="update" v-model="incident.enabled" type="checkbox" class="switch" :id="`switch-incident-${incident.id}`">
<label :for="`switch-incident-${incident.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="loaded" class="col-12 mb-3">
<h3>Notifiers
<button @click.prevent="toggle_all(file.notifiers)" class="btn btn-sm btn-outline-dark float-right mt-1">Select All</button>
</h3>
<div v-if="!file.notifiers" class="alert alert-link">
No Notifiers in file
</div>
<div v-for="notifier in file.notifiers" v-bind:key="notifier.id" class="row">
<div class="col-4">
{{notifier.title}}
</div>
<div class="col-8 text-right">
<span @click="notifier.enabled = !!notifier.enabled" class="switch">
<input @change="update" v-model="notifier.enabled" type="checkbox" class="switch" :id="`switch-notifier-${notifier.id}`">
<label :for="`switch-notifier-${notifier.id}`"></label>
</span>
</div>
</div>
</div>
<div v-if="error" class="alert alert-danger">
{{error}}
</div>
<div class="col-12">
<button v-if="loaded" @click.prevent="import_all" class="btn btn-block btn-success">Import</button>
</div>
</div>
</template>
<script>
import Api from '../../API';
export default {
name: "Importer",
data () {
return {
error: null,
file: null,
loaded: false,
output: null,
all: {
notifiers: false,
services: false,
groups: false,
}
}
},
methods: {
clean_elem(elem) {
if (!elem) {
return null
}
elem.map(e => delete(e.enabled) && delete(e.id))
return elem
},
async import_all() {
this.error = null
const outgoing = {
core: this.output.core,
users: this.clean_elem(this.output.users),
services: this.clean_elem(this.output.services),
groups: this.clean_elem(this.output.groups),
notifiers: this.clean_elem(this.output.notifiers),
checkins: this.clean_elem(this.output.checkins),
}
try {
await Api.import(outgoing)
} catch(e) {
this.error = e
}
},
toggle_all(elem) {
elem.map(s => s.enabled = true)
this.update()
},
update() {
this.output = {
core: this.file.core.enabled ? this.file.core : null,
users: this.file.users.filter(s => s.enabled),
services: this.file.services.filter(s => s.enabled),
groups: this.file.groups.filter(s => s.enabled),
notifiers: this.file.notifiers.filter(s => s.enabled),
checkins: this.file.checkins.filter(s => s.enabled),
}
},
onFileChange(e) {
let files = e.target.files || e.dataTransfer.files;
if (!files.length)
return;
this.processJSON(files[0]);
},
processJSON(file) {
let reader = new FileReader();
reader.onload = (e) => {
this.file = JSON.parse(e.target.result);
this.file.core.enabled = false
};
reader.readAsText(file);
this.loaded = true
},
}
}
</script>
<style scoped>
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class="col-12">
<div v-for="incident in incidents" :key="incident.id" class="card contain-card text-black-50 bg-white mb-4">
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
<font-awesome-icon icon="times" />
</button>
</div>
@ -14,7 +14,7 @@
</div>
<div class="card contain-card text-black-50 bg-white">
<div class="card contain-card">
<div class="card-header">Create Incident</div>
<div class="card-body">
<form @submit.prevent="createIncident">
@ -50,8 +50,9 @@
</template>
<script>
import Api from "../../API";
const FormIncidentUpdates = () => import('@/forms/IncidentUpdates')
import Api from "../../API";
const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/forms/IncidentUpdates')
export default {
name: 'Incidents',
@ -79,15 +80,23 @@
methods: {
async delete(i) {
this.res = await Api.incident_delete(i)
if (this.res.status === "success") {
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
//await this.loadIncidents()
}
},
async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) {
this.res = await Api.incident_delete(incident)
if (this.res.status === "success") {
this.incidents = this.incidents.filter(obj => obj.id !== incident.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
//await this.loadIncidents()
} // TODO: further error checking here... maybe alert user it failed with modal or so
}
const modal = {
visible: true,
title: "Delete Incident",
body: `Are you sure you want to delete Incident ${incident.title}?`,
btnColor: "btn-danger",
btnText: "Delete Incident",
func: () => this.delete(incident),
}
this.$store.commit("setModal", modal)
},
async createIncident() {
@ -112,10 +121,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="row p-2">
<div v-if="loaded && last_failure && failureBefore" class="col-12 text-danger font-2 m-0 mb-2">
<font-awesome-icon icon="exclamation" class="mr-1 text-danger font-weight-bold" size="1x"/> Recent Failure<br>
<span class="font-italic font-weight-light text-dim mt-1" style="max-width: 270px">
Last failure was {{ago(last_failure.created_at)}} ago. {{last_failure.issue}}
</span>
</div>
<div v-if="loaded" v-for="message in messages" class="col-12 font-2 m-0 mb-2">
<font-awesome-icon icon="calendar" class="mr-1" size="1x"/> Upcoming Announcement<br>
<span class="font-italic font-weight-light text-dim mt-1">{{message.description}}</span>
<span class="font-0 text-dim float-right font-weight-light mt-1">@ <strong>{{niceDate(message.start_on)}}</strong>
</span>
</div>
<div v-if="loaded" v-for="incident in incidents" class="col-12 font-2 m-0 mb-2">
<font-awesome-icon icon="bullhorn" class="mr-1" size="1x"/>Recent Incident<br>
<span class="font-italic font-weight-light text-dim mt-1" style="max-width: 270px">{{incident.title}} - {{incident.description}}</span>
<span class="font-0 text-dim float-right font-weight-light mt-1">@ <strong>{{niceDate(incident.created_at)}}</strong></span>
</div>
<div v-if="success_event && !failureBefore" class="col-12 font-2 m-0 mb-2">
<span class="text-success"><font-awesome-icon icon="check" class="mr-1" size="1x"/>No New Events</span>
<span class="font-italic d-inline-block text-truncate text-dim mt-1" style="max-width: 270px">
Last failure was {{ago(service.last_error)}} ago.
</span>
</div>
</div>
</template>
<script>
import Api from "../../API";
const Loading = () => import(/* webpackChunkName: "index" */ "@/components/Elements/Loading");
export default {
name: "ServiceEvents",
components: {
Loading
},
props: {
service: {
type: Object,
required: true
}
},
data() {
return {
incidents: null,
loaded: false,
}
},
mounted() {
this.load()
},
computed: {
last_failure() {
if (!this.service.failures) {
return null
}
return this.service.failures[0]
},
failureBefore() {
return this.isAfter(this.parseISO(this.service.last_error), this.nowSubtract(43200).toISOString())
},
messages() {
return this.$store.getters.serviceMessages(this.service.id)
},
success_event() {
if (this.service.online && this.service.messages.length === 0 && this.service.incidents.length === 0) {
return true
}
return false
}
},
methods: {
async load() {
this.loaded = false
await this.getMessages()
await this.getIncidents()
this.loaded = true
},
async getMessages() {
// this.messages = this.$store.getters.serviceMessages(this.service.id)
},
async getIncidents() {
this.incidents = await Api.incidents_service(this.service.id)
},
}
}
</script>

View File

@ -0,0 +1,173 @@
<template>
<div class="dashboard_card card mb-4" :class="{'offline-card': !service.online}">
<div class="card-header pb-1">
<h6 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)" class="no-decoration">{{service.name}}</router-link>
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</h6>
</div>
<div class="card-body">
<div v-if="loaded" class="row pl-2">
<div class="col-md-6 col-sm-12 pl-2 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div>
<div class="col-md-6 col-sm-12 pl-0 mt-4 mt-md-0 mb-3">
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
</div>
<ServiceEvents :service="service"/>
</div>
<div v-else class="row mb-5">
<div class="col-12 col-md-6 text-center">
<font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div>
<div class="col-12 col-md-6 text-center text-dim">
<font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-5 pr-0">
<span class="small text-dim">{{ hoverbtn }}</span>
</div>
<div class="col-7 pr-2 pl-0">
<div class="btn-group float-right">
<button @click="$router.push({path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover($t('incidents'))" class="btn btn-sm btn-white incident">
<font-awesome-icon icon="bullhorn"/>
</button>
<button @click="$router.push({path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover($t('checkins'))" class="btn btn-sm btn-white checkins">
<font-awesome-icon icon="calendar-check"/>
</button>
<button @click="$router.push({path: `/dashboard/service/${service.id}/failures`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover($t('failures'))" class="btn btn-sm btn-white failures">
<font-awesome-icon icon="exclamation-triangle"/> <span v-if="service.stats.failures !== 0" class="badge badge-danger ml-1">{{service.stats.failures}}</span>
</button>
</div>
</div>
</div>
</div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
{{ $t('failed') }} {{failure.created_at}}<br>
{{failure.issue}}
</span>
</div>
</template>
<script>
const Checkin = () => import(/* webpackChunkName: "dashboard" */ '../../forms/Checkin');
const FormMessage = () => import(/* webpackChunkName: "dashboard" */ '../../forms/Message');
const ServiceFailures = () => import(/* webpackChunkName: "dashboard" */ '../Service/ServiceFailures');
const ServiceSparkLine = () => import(/* webpackChunkName: "dashboard" */ "./ServiceSparkLine");
import Api from "../../API";
const ServiceEvents = () => import(/* webpackChunkName: "dashboard" */ "@/components/Dashboard/ServiceEvents");
export default {
name: 'ServiceInfo',
components: {
ServiceEvents,
Checkin,
ServiceFailures,
FormMessage,
ServiceSparkLine
},
props: {
service: {
type: Object,
required: true
}
},
data() {
return {
uptime: null,
hovered: false,
hoverbtn: "",
openTab: "",
set1: [],
set2: [],
loaded: false,
set1_name: "",
set2_name: "",
failures: null,
visible: false
}
},
watch: {
},
mounted() {
this.unsetHover()
},
methods: {
setHover(name) {
this.hoverbtn = name
},
unsetHover() {
this.hoverbtn = this.$t('uptime', [this.service.online_7_days])
},
async setVisible(isVisible, entry) {
if (isVisible && !this.visible) {
await this.loadInfo()
await this.getUptime()
this.visible = true
}
},
async getUptime() {
const end = this.endOf("day", this.now())
const start = this.beginningOf("day", this.nowSubtract(3 * 86400))
this.uptime = await Api.service_uptime(this.service.id, this.toUnix(start), this.toUnix(end))
},
async loadInfo() {
this.set1 = await this.getHits(86400 * 7, "12h")
this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(86400, "60m")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
Tab(name) {
if (this.openTab === name) {
this.openTab = ''
return
}
this.openTab=name;
},
sinceYesterday(data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.y)
});
total = total / data.length
},
async getHits(seconds, group) {
let start = this.nowSubtract(seconds)
let end = this.endOf("today")
const startEnd = this.startEndParams(start, end, group)
const fetched = await Api.service_hits(this.service.id, startEnd.start, startEnd.end, group, true)
const data = this.convertToChartData(fetched, 0.001, true)
return [{name: "Latency", ...data}]
},
calc(s) {
let data = s[0].data
if (data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.y)
});
total = total / data.length
return Math.round(total) + " ms"
} else {
return "Offline"
}
}
}
}
</script>

View File

@ -1,11 +1,10 @@
<template v-if="series.length">
<apexchart width="100%" height="180" type="bar" :options="chartOpts" :series="series"></apexchart>
<apexchart width="100%" height="100" type="bar" :options="chartOpts" :series="series"></apexchart>
</template>
<script>
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: 'ServiceSparkLine',
props: {
@ -38,6 +37,9 @@
enabled: true
},
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: {
curve: 'straight'
},
@ -51,21 +53,20 @@
tooltip: {
theme: false,
enabled: true,
custom: function({series, seriesIndex, dataPointIndex, w}) {
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
val = val + " ms"
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
return `<div class="chartmarker"><span class="font-3">Average Response Time: ${this.humanTime(val)}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
position: 'bottomLeft',
offsetX: 0,
offsetY: 0,
offsetY: -30,
},
x: {
show: false,
show: true,
},
y: {
formatter: (value) => { return value + " %" },
@ -75,15 +76,16 @@
text: this.title,
offsetX: 0,
style: {
fontSize: '28px',
fontSize: '18px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: this.subtitle,
offsetX: 0,
offsetY: 20,
style: {
fontSize: '14px',
fontSize: '9px',
cssClass: 'apexcharts-yaxis-title'
}
}
@ -92,7 +94,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,10 +1,26 @@
<template>
<table class="table">
<div>
<div v-if="servicesList.length === 0">
<div class="alert alert-dark d-block mt-3 mb-0">
You currently don't have any services!
</div>
</div>
<table v-else class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
<th scope="col">{{$t('name')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{$t('status')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{$t('visibility')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('group') }}</th>
<th scope="col" class="d-none d-md-table-cell" style="width: 130px">
{{$t('failures')}}
<div class="btn-group float-right" role="group">
<a @click="list_timeframe='3h'" type="button" class="small" :class="{'text-success': list_timeframe==='3h', 'text-muted': list_timeframe!=='3h'}">3h</a>
<a @click="list_timeframe='12h'" type="button" class="small" :class="{'text-success': list_timeframe==='12h', 'text-muted': list_timeframe!=='12h'}">12h</a>
<a @click="list_timeframe='24h'" type="button" class="small" :class="{'text-success': list_timeframe==='24h', 'text-muted': list_timeframe!=='24h'}">24h</a>
<a @click="list_timeframe='7d'" type="button" class="small" :class="{'text-success': list_timeframe==='7d', 'text-muted': list_timeframe!=='7d'}">7d</a>
</div>
</th>
<th scope="col"></th>
</tr>
</thead>
@ -15,6 +31,11 @@
<font-awesome-icon icon="bars" class="mr-3"/>
</span> {{service.name}}
</td>
<td class="d-none d-md-table-cell">
<span class="badge text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</td>
<td class="d-none d-md-table-cell">
<span class="badge text-uppercase" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
{{service.public ? $t('public') : $t('private')}}
@ -25,15 +46,18 @@
<span class="badge badge-secondary">{{serviceGroup(service)}}</span>
</div>
</td>
<td class="d-none d-md-table-cell">
<ServiceSparkList :service="service" :timeframe="list_timeframe"/>
</td>
<td class="text-right">
<div class="btn-group">
<button v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="edit" />
</button>
<button @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<button :disabled="loading" @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="chart-area" />
</button>
<button v-if="$store.state.admin" @click.prevent="deleteService(service)" href="#" class="btn btn-sm btn-danger">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="deleteService(service)" class="btn btn-sm btn-danger">
<font-awesome-icon v-if="!loading" icon="times" />
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</button>
@ -42,22 +66,77 @@
</tr>
</draggable>
</table>
</div>
</template>
<script>
import Api from "../../API";
import draggable from 'vuedraggable'
import ToggleSwitch from '../../forms/ToggleSwitch';
import ServiceSparkList from "@/components/Service/ServiceSparkList";
import Modal from "@/components/Elements/Modal";
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch');
export default {
name: 'ServicesList',
components: {
Modal,
ServiceSparkList,
ToggleSwitch,
draggable
},
data() {
return {
loading: false,
list_timeframe: "12h",
chartOpts: {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
},
xaxis: {
type: 'numeric',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.8,
},
yaxis: {
min: 0
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#39c10a'
}, {
from: 2,
to: 90,
color: '#e01a1a'
}]
},
},
},
tooltip: {
theme: false,
enabled: false,
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
}
},
computed: {
@ -82,14 +161,25 @@ export default {
await Api.services_reorder(data)
await this.update()
},
tester(s) {
console.log(s)
},
async delete(s) {
this.loading = true
await Api.service_delete(s.id)
await this.update()
this.loading = false
},
async deleteService(s) {
let c = confirm(`Are you sure you want to delete '${s.name}'?`)
if (c) {
this.loading = true
await Api.service_delete(s.id)
await this.update()
this.loading = false
}
const modal = {
visible: true,
title: "Delete Service",
body: `Are you sure you want to delete service ${s.name}? This will also delete all failures, checkins, and incidents for this service.`,
btnColor: "btn-danger",
btnText: "Delete Service",
func: () => this.delete(s),
}
this.$store.commit("setModal", modal)
},
serviceGroup(s) {
let group = this.$store.getters.groupById(s.group_id)

View File

@ -1,17 +1,21 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card-header">Theme Editor</div>
<div class="card mb-5">
<div class="card-header">{{$t('theme_editor')}}</div>
<div class="card-body">
<div v-if="error" class="alert alert-danger mt-3" style="white-space: pre-line;">
{{error}}
</div>
<h6 v-if="directory" id="assets_dir" class="text-muted text-monospace text-sm-center font-1 mb-4">
{{$t('assets_dir')}}: {{directory}}
</h6>
<div v-if="loaded && !directory" class="jumbotron jumbotron-fluid">
<div class="text-center col-12">
<h1 class="display-5">Enable Local Assets</h1>
<span class="lead">Customize your status page design by enabling local assets. This will create a 'assets' directory containing all CSS.<p>
<h1 class="display-5">{{$t('enable_assets')}}</h1>
<span class="lead">{{$t('assets_desc')}}<p>
<button id="enable_assets" @click.prevent="createAssets" :disabled="pending" href="#" class="btn btn-primary mt-3">
<font-awesome-icon v-if="pending" icon="circle-notch" class="mr-2" spin/>{{pending ? "Creating Assets" : "Enable Local Assets"}}
<font-awesome-icon v-if="pending" icon="circle-notch" class="mr-2" spin/>{{pending ? $t('assets_loading') : $t('assets_btn')}}
</button>
</p></span>
</div>
@ -21,33 +25,48 @@
<h3>Variables</h3>
<codemirror v-show="loaded" v-model="vars" ref="vars" :options="cmOptions" class="codemirrorInput"/>
<h3 class="mt-3">Base Theme</h3>
<h3 class="mt-3">Base {{$t('theme')}}</h3>
<codemirror v-show="loaded" v-model="base" ref="base" :options="cmOptions" class="codemirrorInput"/>
<h3 class="mt-3">Layout {{$t('theme')}}</h3>
<codemirror v-show="loaded" v-model="layout" ref="layout" :options="cmOptions" class="codemirrorInput"/>
<h3 class="mt-3">Forms {{$t('theme')}}</h3>
<codemirror v-show="loaded" v-model="forms" ref="forms" :options="cmOptions" class="codemirrorInput"/>
<h3 class="mt-3">Mixins</h3>
<codemirror v-show="loaded" v-model="mixins" ref="mixins" :options="cmOptions" class="codemirrorInput"/>
<h3 class="mt-3">Mobile Overwrites</h3>
<codemirror v-show="loaded" v-model="mobile" ref="mobile" :options="cmOptions" class="codemirrorInput"/>
<button id="save_assets" @submit.prevent="saveAssets" type="submit" class="btn btn-primary btn-block mt-2" :disabled="pending">{{pending ? "Saving..." : "Save Style"}}</button>
<button id="delete_assets" v-if="directory" @click.prevent="deleteAssets" href="#" class="btn btn-danger btn-block confirm-btn" :disabled="pending">Delete Local Assets</button>
<h6 id="assets_dir" class="text-muted text-monospace text-sm-center font-1 mt-3">
Asset Directory: {{directory}}
</h6>
</form>
</div>
<div v-if="directory" class="card-footer">
<div class="row">
<div class="col-6">
<button id="save_assets" @click.prevent="saveAssets" type="submit" class="btn btn-primary btn-block" :disabled="pending">{{pending ? "Saving..." : "Save Styles"}}</button>
</div>
<div class="col-6">
<button id="delete_assets" @click.prevent="deleteAssets" class="btn btn-danger btn-block confirm-btn" :disabled="pending">Delete Local Assets</button>
</div>
</div>
</div>
</div>
</template>
<script>
import Api from "../../API";
import Api from "../../API";
// require component
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/css/css.js'
// require component
import {codemirror} from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror-colorpicker/dist/codemirror-colorpicker.css'
import 'codemirror-colorpicker'
import('codemirror/mode/css/css.js')
import('codemirror/lib/codemirror.css')
import('codemirror-colorpicker/dist/codemirror-colorpicker.css')
import('codemirror-colorpicker')
export default {
name: 'ThemeEditor',
@ -62,6 +81,9 @@
data () {
return {
base: null,
layout: null,
forms: null,
mixins: null,
vars: null,
mobile: null,
error: null,
@ -90,6 +112,9 @@
this.$refs.vars.codemirror.refresh()
this.$refs.base.codemirror.refresh()
this.$refs.mobile.codemirror.refresh()
this.$refs.layout.codemirror.refresh()
this.$refs.forms.codemirror.refresh()
this.$refs.mixins.codemirror.refresh()
}
},
async fetchTheme() {
@ -101,6 +126,9 @@
this.base = theme.base
this.vars = theme.variables
this.mobile = theme.mobile
this.layout = theme.layout
this.forms = theme.forms
this.mixins = theme.mixins
}
this.pending = false
this.loaded = true
@ -116,18 +144,33 @@
this.pending = false
await this.fetchTheme()
},
async delete() {
this.pending = true
const resp = await Api.theme_generate(false)
await this.fetchTheme()
this.pending = false
},
async deleteAssets() {
this.pending = true
let c = confirm('Are you sure you want to delete all local assets?')
if (c) {
const resp = await Api.theme_generate(false)
await this.fetchTheme()
}
this.pending = false
const modal = {
visible: true,
title: "Delete Local Assets",
body: `Are you sure you want to delete all local assets?`,
btnColor: "btn-danger",
btnText: "Delete",
func: () => this.delete(),
}
this.$store.commit("setModal", modal)
},
async saveAssets() {
this.pending = true
const data = {base: this.base, variables: this.vars, mobile: this.mobile}
const data = {
base: this.base,
layout: this.layout,
forms: this.forms,
mixins: this.mixins,
variables: this.vars,
mobile: this.mobile
}
let resp
try {
resp = await Api.theme_save(data)
@ -157,9 +200,3 @@
}
}
</script>
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<nav class="navbar navbar-expand-lg">
<router-link to="/" class="navbar-brand">Statping</router-link>
<button @click="navopen = !navopen" class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<font-awesome-icon v-if="!navopen" icon="bars"/>
@ -9,26 +9,29 @@
<div class="navbar-collapse" :class="{collapse: !navopen}" id="navbarText">
<ul class="navbar-nav mr-auto">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard" class="nav-link">{{ $t('top_nav.dashboard') }}</router-link>
<router-link to="/dashboard" class="nav-link">{{ $t('dashboard') }}</router-link>
</li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/services" class="nav-link">{{ $t('top_nav.services') }}</router-link>
<router-link to="/dashboard/services" class="nav-link">{{ $t('services') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/users" class="nav-link">{{ $t('top_nav.users') }}</router-link>
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/users" class="nav-link">{{ $t('users') }}</router-link>
</li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/messages" class="nav-link">{{ $t('top_nav.announcements') }}</router-link>
<router-link to="/dashboard/messages" class="nav-link">{{ $t('announcements') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">{{ $t('top_nav.settings') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">{{ $t('top_nav.logs') }}</router-link>
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">{{ $t('settings') }}</router-link>
</li>
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">{{ $t('logs') }}</router-link>
</li>
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/help" class="nav-link">{{ $t('help') }}</router-link>
</li>
</ul>
<span class="navbar-text">
<a href="#" class="nav-link" @click.prevent="logout">{{ $t('top_nav.logout') }}</a>
<a href="#" class="nav-link" @click.prevent="logout">{{ $t('logout') }}</a>
</span>
</div>
</nav>
@ -37,7 +40,6 @@
<script>
import Api from "../../API"
import Vue from "vue";
export default {
name: 'TopNav',
@ -46,6 +48,11 @@
navopen: false
}
},
computed: {
admin() {
return this.$store.state.admin
}
},
methods: {
async logout () {
await Api.logout()
@ -58,7 +65,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='investigating', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
<span class="text-muted">- {{update.message}}
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
<span class="d-block small">{{ago(update.created_at)}} ago</span>
</div>
</template>
<script>
import Api from "@/API";
export default {
name: "IncidentUpdate",
props: {
update: {
required: true
},
admin: {
required: true
},
onUpdate: {
required: false
}
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.onUpdate()
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<div v-if="loading" class="row mt-3 mb-3">
<div class="col-12 text-center text-muted">
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div>
</div>
</template>
<script>
export default {
name: "Loading",
props: {
loading: {
type: Boolean,
required: true,
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,50 @@
<template>
<div v-if="modal.visible" class="modal d-block mt-5 pt-5" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{modal.title}}</h5>
</div>
<div class="modal-body">
<p>{{modal.body}}</p>
</div>
<div class="modal-footer">
<button @click.prevent="close" type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button @click.prevent="runFunc" type="button" :class="`btn ${modal.btnColor}`">{{modal.btnText}}</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Modal",
data () {
return {
}
},
computed: {
modal() {
return this.$store.getters.modal
}
},
mounted() {
},
methods: {
runFunc() {
this.$store.getters.modal.func()
this.close()
},
close() {
this.$store.commit("setModal", {visible: false})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,46 @@
const serviceSparkLine = {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.3,
},
yaxis: {
min: 0
},
colors: ['#b3bdc3'],
tooltip: {
theme: false,
enabled: false,
},
title: {
text: this.title,
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: this.subtitle,
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
}
}
export default {
ServiceList: serviceSparkLine
}

View File

@ -1,10 +1,15 @@
<template>
<footer>
<div v-if="!core.footer" class="footer text-center mb-4 p-2">
<a href="https://github.com/statping/statping" target="_blank">
Statping {{core.version}} made with <font-awesome-icon icon="heart" class="text-danger"/>
</a> |
<router-link :to="$store.state.admin ? '/dashboard' : '/login'">{{$t('top_nav.dashboard')}}</router-link>
<div class="d-block text-dim">
<div class="mb-3">
<router-link class="links" :to="admin ? '/dashboard' : '/login'">{{$t('dashboard')}}</router-link>
</div>
<span class="font-1 mt-3">
<a href="https://github.com/statping/statping" class="statping" target="_blank">
Statping v{{core.version}} made with <font-awesome-icon icon="heart" class="hlight font-1"/></a>
</span>
</div>
</div>
<div v-else class="footer text-center mb-4 p-2" v-html="core.footer"></div>
</footer>
@ -21,11 +26,19 @@
computed: {
core() {
return this.$store.getters.core
}
},
commit() {
return this.$store.getters.core.commit.slice(0,8)
},
admin() {
return this.$store.getters.admin
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.hlight {
color: #f6cbcb;
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<div class="col-12 full-col-12">
<div v-if="services.length > 0" class="col-12 full-col-12">
<h4 v-if="group.name !== 'Empty Group'" class="group_header mb-3 mt-4">{{group.name}}</h4>
<div class="list-group online_list mb-4">
<a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<div v-for="(service, index) in services" v-bind:key="index" class="list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">
{{service.online ? $t('online') : $t('offline')}}
@ -12,16 +12,16 @@
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
</div>
</div>
</template>
<script>
import Api from '../../API';
import GroupServiceFailures from './GroupServiceFailures';
import IncidentsBlock from './IncidentsBlock';
const GroupServiceFailures = () => import(/* webpackChunkName: "index" */ './GroupServiceFailures');
const IncidentsBlock = () => import(/* webpackChunkName: "index" */ './IncidentsBlock');
export default {
name: 'Group',
@ -30,11 +30,15 @@ export default {
GroupServiceFailures
},
props: {
group: Object
group: {
type: Object,
required: true,
}
},
computed: {
services() {
return this.$store.getters.servicesInGroup(this.group.id)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,17 +1,31 @@
<template>
<div>
<div class="d-flex mt-3 mb-2">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'mini_error': d.amount > 0, 'mini_success': d.amount === 0}">
<span v-if="d.amount != 0" class="small">{{d.amount}}</span>
<div v-observe-visibility="{callback: visibleChart, once: true}" v-if="!loaded" class="row">
<div class="col-12 text-center mt-3">
<font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div>
</div>
<transition name="fade">
<div v-if="loaded">
<div class="d-flex mt-3">
<div class="flex-fill service_day" v-for="(d, index) in failureData" @mouseover="mouseover(d)" @mouseout="mouseout" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small"></span>
</div>
</div>
<div class="row mt-2">
<div class="col-3 text-left font-2 text-muted">30 Days Ago</div>
<div class="col-6 text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}">
{{service_txt}}
</div>
<div class="col-3 text-right font-2 text-muted">Today</div>
<div class="col-12 no-select">
<p class="divided">
<span class="font-2 text-muted">90 {{$t('days_ago')}}</span>
<span class="divider"></span>
<span class="text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}">{{service_txt}}</span>
<span class="divider"></span>
<span class="font-2 text-muted">{{$t('today')}}</span>
</p>
</div>
</div>
<div class="daily-failures small text-right text-dim">{{hover_text}}</div>
</div>
</transition>
</div>
</template>
@ -26,6 +40,9 @@ export default {
data() {
return {
failureData: [],
hover_text: "",
loaded: false,
visible: false,
}
},
props: {
@ -39,22 +56,35 @@ export default {
return this.smallText(this.service)
}
},
mounted () {
this.lastDaysFailures()
mounted () {
},
methods: {
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
this.lastDaysFailures().then(() => this.loaded = true)
}
},
mouseout() {
this.hover_text = ""
},
mouseover(e) {
let txt = `${e.amount} Failures`
if (e.amount === 0) {
txt = `No Issues`
}
this.hover_text = `${e.date.toLocaleDateString()} - ${txt}`
},
async lastDaysFailures() {
const start = this.nowSubtract(86400 * 30)
const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.startToday()), "24h")
const start = this.beginningOf('day', this.nowSubtract(86400 * 90))
const end = this.endOf('tomorrow')
const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(end), "24h", true)
data.forEach((d) => {
let date = this.parseISO(d.timeframe)
this.failureData.push({month: 1, day: date.getDate(), amount: d.amount})
this.failureData.push({month: date.getMonth(), day: date.getDate(), date: date, amount: d.amount})
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -15,7 +15,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,25 +1,25 @@
<template>
<div class="row">
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
<div v-for="(incident, i) in incidents" class="col-12 mt-2">
<span class="braker mt-1 mb-3"></span>
<h6>Incident: {{incident.title}}
<h6>{{incident.title}}
<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>
</h6>
<span class="font-2" v-html="incident.description"></span>
<UpdatesBlock :incident="incident"/>
<div class="font-2 mb-3" v-html="incident.description"></div>
<IncidentUpdate v-for="(update, i) in incident.updates" v-bind:key="i" :update="update" :admin="false"/>
</div>
</div>
</template>
<script>
import Api from '../../API';
import UpdatesBlock from "@/components/Index/UpdatesBlock";
import IncidentUpdate from "@/components/Elements/IncidentUpdate";
export default {
name: 'IncidentsBlock',
components: {UpdatesBlock},
components: {
IncidentUpdate
},
props: {
service: {
type: Object,
@ -49,8 +49,7 @@ export default {
this.incidents = await Api.incidents_service(this.service.id)
},
async incident_updates(incident) {
await Api.incident_updates(incident).then((d) => {return d})
return o
return await Api.incident_updates(incident)
}
}
}

View File

@ -1,13 +1,20 @@
<template>
<div class="alert alert-warning pb-4 pt-3 mt-5 mb-5" role="alert">
<h3 class="mb-3">{{message.title}}</h3>
<span class="mb-3">{{message.description}}</span>
<div class="row d-block mt-3">
<span class="col-12 col-md-6 text-left small">
Started {{niceDate(message.start_on)}} ({{ago(message.start_on)}} ago)
</span>
<span class="col-12 col-md-6 text-right float-right small">
Ends on {{niceDate(message.end_on)}} (in {{ago(message.end_on)}})</span>
<div class="card shadow mb-4" role="alert">
<div class="card-body pb-2">
<h3 class="mb-3 font-weight-bold">{{message.title}}</h3>
<span class="mb-2">{{message.description}}</span>
<div class="col-12 mb-0">
<div class="dates">
<div class="start">
<strong>STARTS</strong> {{niceDate(message.start_on)}}
<span></span>
</div>
<div class="ends">
<strong>ENDS</strong> {{niceDate(message.end_on)}}
</div>
</div>
</div>
</div>
</div>
</template>
@ -17,7 +24,8 @@ export default {
name: 'MessageBlock',
props: {
message: {
type: Object
type: Object,
required: true,
}
}
}
@ -25,4 +33,5 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,52 +0,0 @@
<template>
<div class="row">
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
<div class="col-md-2 col-12">
<span class="badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
</div>
<div class="col-md-12 col-12 mt-2 font-3">{{update.message}}</div>
<div class="col-12 font-1 float-right text-black-50 mt-2">{{ago(update.created_at)}} ago</div>
</div>
</div>
</template>
<script>
import Api from '../../API';
export default {
name: 'UpdatesBlock',
props: {
incident: {
type: Object,
required: true
}
},
data() {
return {
updates: null,
}
},
mounted () {
this.getIncidentUpdates()
},
methods: {
badgeClass(val) {
switch (val.toLowerCase()) {
case "resolved":
return "badge-success"
case "update":
return "badge-info"
case "investigating":
return "badge-danger"
}
},
async getIncidentUpdates() {
this.updates = await Api.incident_updates(this.incident)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,8 +1,6 @@
<template>
<div>
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
<div class="service-chart-container">
<apexchart width="100%" height="350" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</template>
@ -38,6 +36,7 @@
return {
loading: true,
main_data: null,
ping_data: null,
expanded_data: null,
main_chart_options: {
noData: {
@ -53,6 +52,7 @@
},
chart: {
id: 'mainchart',
stacked: true,
events: {
dataPointSelection: (event, chartContext, config) => {
window.console.log('slect')
@ -112,7 +112,9 @@
},
yaxis: {
labels: {
show: true
formatter: (value) => {
return this.humanTime(value)
}
},
},
markers: {
@ -167,15 +169,15 @@
show: false
},
fill: {
colors: ["#48d338"],
colors: ["#f1771f", "#48d338"],
opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
curve: 'stepline',
lineCap: 'butt',
colors: ["#3aa82d"],
colors: ["#f1771f", "#48d338"],
width: 2,
}
},
@ -229,8 +231,11 @@
computed: {
main_chart () {
return [{
name: this.service.name,
name: "latency",
...this.convertToChartData(this.main_data)
},{
name: "ping",
...this.convertToChartData(this.ping_data)
}]
},
expanded_chart () {
@ -263,9 +268,13 @@
},
async chartHits() {
this.main_data = await this.load_hits()
this.ping_data = await this.load_ping()
},
async load_hits(start=this.params.start, end=this.params.end, group=this.group) {
return await Api.service_hits(this.service.id, start, end, group, false)
},
async load_ping(start=this.params.start, end=this.params.end, group=this.group) {
return await Api.service_ping(this.service.id, start, end, group, false)
}
}
}

View File

@ -14,7 +14,7 @@
<script>
import MiniSparkLine from './MiniSparkLine';
import ServiceSparkLine from './ServiceSparkLine';
import ServiceSparkLine from '../Dashboard/ServiceSparkLine';
export default {
name: 'Analytics',

View File

@ -0,0 +1,167 @@
<template>
<div class="col-12">
<div class="text-center" style="width:210px" v-if="!loaded">
<font-awesome-icon icon="circle-notch" class="h-25 text-dim" spin/>
</div>
<apexchart v-else width="100%" height="50" type="bar" :options="chartOpts" :series="data"></apexchart>
</div>
</template>
<script>
import Api from "@/API";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: "FailuresBarChart",
props: {
service: {
required: true,
type: Object,
},
group: {
required: true,
type: String,
},
start: {
required: true,
type: String,
},
end: {
required: true,
type: String,
},
},
data() {
return {
data: null,
loaded: false,
chartOpts: {
chart: {
type: 'bar',
height: 150,
sparkline: {
enabled: true
},
animations: {
enabled: false,
},
},
xaxis: {
type: 'datetime',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 80},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.4,
},
yaxis: {
min: 0,
max: 1,
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#cfcfcf'
}, {
from: 2,
to: 3,
color: '#f58e49'
}, {
from: 3,
to: 20,
color: '#e01a1a'
}, {
from: 21,
to: Infinity,
color: '#9b0909'
}]
},
},
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let val = series[seriesIndex][dataPointIndex];
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let ago = `${(dataPointIndex-12) * -1} hours ago`
if ((dataPointIndex-12) * -1 === 0) {
ago = `Current hour`
}
return `<div class="chart_list_tooltip font-2 mb-4">${val-1} Failures<br>${dt}</div>`
},
fixed: {
enabled: true,
position: 'topLeft',
offsetX: 0,
offsetY: 0,
},
x: {
formatter: (value) => { return value },
},
y: {
show: false
},
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
}
},
async mounted() {
await this.loadFailures()
},
watch: {
group(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
start(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
end(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
},
methods: {
convertChartData(data) {
if (!data) {
return []
}
let arr = []
data.forEach((d, k) => {
arr.push({
x: d.timeframe,
y: d.amount+1,
})
})
return arr
},
async loadFailures() {
this.loaded = false
const startEnd = this.startEndParams(this.parseISO(this.start), this.parseISO(this.end), this.group)
const data = await Api.service_failures_data(this.service.id, startEnd.start, startEnd.end, this.group, true)
this.loaded = true
this.data = [{data: this.convertChartData(data)}]
}
},
}
</script>

View File

@ -1,21 +0,0 @@
<template>
<div class="row stats_area mt-5 mb-4">
okok
</div>
</template>
<script>
export default {
name: 'Incidents',
props: {
service: {
type: Object,
required: true
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -10,24 +10,10 @@
<ServiceTopStats :service="service"/>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" :func="stats.total_failures"/>
<Analytics title="Total Failures" :func="stats.total_failures"/>
<Analytics title="Highest Latency" :func="stats.high_latency"/>
<Analytics title="Lowest Latency" :func="stats.lowest_latency"/>
<Analytics title="Total Uptime" :func="stats.high_ping"/>
<Analytics title="Total Downtime" :func="stats.low_ping"/>
<div class="col-12">
<router-link :to="serviceLink(service)" class="btn btn-block btn-outline-success mt-4" :class="{'btn-outline-success': service.online, 'btn-outline-danger': !service.online}">
View More Details
</router-link>
</div>
</div>
</div>
</div>
<div v-show="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<div v-show="!expanded" v-observe-visibility="{callback: visibleChart, throttle: 200}" class="chart-container">
<ServiceChart :service="service" :visible="visible" :chart_timeframe="chartTimeframe"/>
</div>
@ -62,7 +48,7 @@
<div class="col-md-2 col-6 float-right">
<button v-if="!expanded" @click="setService" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{$t('service.view')}}
{{$t('view')}}
</button>
</div>
</div>
@ -72,28 +58,20 @@
</template>
<script>
import Api from '../../API';
import Analytics from './Analytics';
import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import Graphing from '../../graphing'
const Analytics = () => import(/* webpackChunkName: "service" */ './Analytics');
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
const ServiceTopStats = () => import(/* webpackChunkName: "service" */ "@/components/Service/ServiceTopStats");
export default {
name: 'ServiceBlock',
components: { Analytics, ServiceTopStats, ServiceChart},
props: {
in_service: {
service: {
type: Object,
required: true
},
},
watch: {
},
computed: {
service() {
return this.track_service
},
timeframepick() {
return this.timeframes.find(s => s.value === this.timeframe_val)
},
@ -139,7 +117,7 @@ export default {
{value: "4320m", text: "3/day", set: 10 },
{value: "10080m", text: "7/day", set: 11 },
],
stats: {
stats: {
total_failures: {
title: "Total Failures",
subtitle: "Last 7 Days",
@ -166,14 +144,13 @@ export default {
value: 0,
}
},
track_service: null,
}
},
beforeDestroy() {
// clearInterval(this.timer_func)
},
async created() {
this.track_service = this.in_service
created() {
},
methods: {
disabled_interval(interval) {
@ -204,30 +181,8 @@ export default {
},
async setService() {
await this.$store.commit('setService', this.service)
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}})
this.$router.push('/service/'+this.service.id, {props: {service: this.service}})
},
async showMoreStats() {
this.expanded = !this.expanded;
const failData = await Graphing.failures(this.service, 7)
this.stats.total_failures.chart = failData.data;
this.stats.total_failures.value = failData.total;
const hitsData = await Graphing.hits(this.service, 7)
this.stats.high_latency.chart = hitsData.chart;
this.stats.high_latency.value = this.humanTime(hitsData.high);
this.stats.lowest_latency.chart = hitsData.chart;
this.stats.lowest_latency.value = this.humanTime(hitsData.low);
const pingData = await Graphing.pings(this.service, 7)
this.stats.high_ping.chart = pingData.chart;
this.stats.high_ping.value = this.humanTime(pingData.high);
this.stats.low_ping.chart = pingData.chart;
this.stats.low_ping.value = this.humanTime(pingData.low);
},
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
@ -242,7 +197,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -47,122 +47,138 @@
return {
ready: false,
showing: false,
data: [],
chartOptions: {
noData: {
text: 'Loading...'
},
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: true,
initialAnimation: {
enabled: true
}
},
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
grid: {
show: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: -10,
}
},
dropShadow: {
enabled: false,
},
xaxis: {
type: "datetime",
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: false
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
if (val >= 10000) {
val = Math.round(val / 1000) + " ms"
} else {
val = val + " μs"
}
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 0,
},
x: {
show: false,
},
y: {
formatter: (value) => { return value + " %" },
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: false
},
axisBorder: {
show: false
},
fill: {
colors: [this.service.online ? "#48d338" : "#dd3545"],
opacity: 1,
type: 'solid'
},
stroke: {
show: false,
curve: 'smooth',
lineCap: 'butt',
colors: [this.service.online ? "#3aa82d" : "#dd3545"],
}
},
series: [{
data: []
}]
data: null,
ping_data: null,
series: null,
}
},
computed: {
chartOptions() {
return {
noData: {
text: 'Loading...'
},
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: false,
delay: 400,
},
dynamicAnimation: {
enabled: true,
speed: 500
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0,
},
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
grid: {
show: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: -10,
}
},
dropShadow: {
enabled: false,
},
xaxis: {
type: "datetime",
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: false
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[0][dataPointIndex];
let pingVal = series[1][dataPointIndex];
return `<div class="chartmarker">
<span>Average Response Time: ${this.humanTime(val)}/${this.chart_timeframe.interval}</span>
<span>Average Ping: ${this.humanTime(pingVal)}/${this.chart_timeframe.interval}</span>
<span>${dt}</span>
</div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 0,
},
x: {
show: false,
},
y: {
formatter: (value) => {
return value + " %"
},
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: false
},
axisBorder: {
show: false
},
fill: {
colors: this.service.online ? ["#3dc82f", "#48d338"] : ["#c60f20", "#dd3545"],
opacity: 1,
type: 'solid',
},
stroke: {
show: false,
curve: 'smooth',
lineCap: 'butt',
colors: this.service.online ? ["#38bc2a", "#48d338"] : ["#c60f20", "#dd3545"],
}
}
}
},
watch: {
visible: function(newVal, oldVal) {
if (newVal && !this.showing) {
@ -178,17 +194,17 @@
},
methods: {
async chartHits(val) {
const start = val.start_time
const end = this.toUnix(new Date())
this.data = await Api.service_hits(this.service.id, start, end, val.interval, false)
if (this.data === null && val.interval !== "5m") {
await this.chartHits({start_time: val.start_time, interval: "5m"})
}
this.series = [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
this.ready = true
this.ready = false
const end = this.endOf("hour", this.now())
const start = this.beginningOf("hour", this.fromUnix(val.start_time))
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
this.ping_data = await Api.service_ping(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
this.series = [
{name: "Latency", ...this.convertToChartData(this.data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
]
this.ready = true
}
}
}

View File

@ -32,7 +32,7 @@
</template>
<script>
import ServiceChart from "./ServiceChart";
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
import Api from "../../API";
export default {

View File

@ -1,211 +0,0 @@
<template>
<div class="card mb-4" :class="{'offline-card': !service.online}">
<div class="card-title px-4 pt-3">
<h4 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</h4>
</div>
<div class="card-body p-3 p-md-1 pt-md-1 pb-md-1">
<transition name="fade">
<div v-if="loaded" class="col-12 pb-2">
<div v-if="false" class="row mb-4 align-content-center">
<div v-if="!service.online" class="col-3 text-left">
<span class="text-danger font-5 font-weight-bold"></span>
<span class="font-2 d-block">Current Downtime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{service.online_24_hours.toString()}} %
</span>
<span class="font-2 d-block">Total Uptime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
0
</span>
<span class="font-2 d-block">Downtime Today</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{(uptime.uptime / 10000).toFixed(0).toString()}}
</span>
<span class="font-2 d-block">Uptime Duration</span>
</div>
<div class="col-3 text-left">
<span class="text-danger font-5 font-weight-bold">
{{service.failures_24_hours}}
</span>
<span class="font-2 d-block">Failures last 24 hours</span>
</div>
</div>
<div class="row">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div>
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3">
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
</div>
</div>
</div>
</transition>
</div>
<div class="card-footer">
<div class="row">
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize incident">
{{$tc('incident', 2)}}
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize checkins">
{{$tc('checkin', 2)}}
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/failures`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize failures">
{{$tc('failure', 2)}} <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
<span class="text-black-50 float-md-right">
{{$t('uptime', [service.online_7_days])}}
</span>
</div>
</div>
</div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
Failed {{failure.created_at}}<br>
{{failure.issue}}
</span>
</div>
</template>
<script>
import Checkin from '../../forms/Checkin';
import FormIncident from '../../forms/Incident';
import FormMessage from '../../forms/Message';
import ServiceFailures from './ServiceFailures';
import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../../API";
export default {
name: 'ServiceInfo',
components: {
Checkin,
ServiceFailures,
FormIncident,
FormMessage,
ServiceSparkLine
},
props: {
service: {
type: Object,
required: true
}
},
data() {
return {
uptime: null,
openTab: "",
set1: [],
set2: [],
loaded: false,
set1_name: "",
set2_name: "",
failures: null,
visible: false
}
},
watch: {
},
methods: {
async setVisible(isVisible, entry) {
if (isVisible && !this.visible) {
await this.loadInfo()
await this.getUptime()
this.visible = true
}
},
async getUptime() {
this.uptime = await Api.service_uptime(this.service.id)
},
async loadInfo() {
this.set1 = await this.getHits(24 * 7, "6h")
this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24, "1h")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
Tab(name) {
if (this.openTab === name) {
this.openTab = ''
return
}
this.openTab=name;
},
sinceYesterday(data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.y)
});
total = total / data.length
},
async getHits(hours, group) {
const start = this.nowSubtract(3600 * hours)
const fetched = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group, false)
const data = this.convertToChartData(fetched, 0.001, true)
return [{name: "Latency", ...data}]
},
calc(s) {
let data = s[0].data
if (data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.y)
});
total = total / data.length
return Math.round(total) + " ms"
} else {
return "Offline"
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.offline-card {
background-color: #fff5f5;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .75s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<div class="text-center" style="width:210px" v-if="!loaded">
<font-awesome-icon icon="circle-notch" class="h-25 text-dim" spin/>
</div>
<apexchart v-else width="240" height="30" type="bar" :options="chartOpts" :series="data"></apexchart>
</template>
<script>
import Api from "@/API";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: "ServiceSparkList",
props: {
service: {
required: true,
type: Object,
},
timeframe: {
required: true,
type: String,
}
},
data() {
return {
data: null,
loaded: false,
chartOpts: {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
animations: {
enabled: false,
},
},
xaxis: {
type: 'datetime',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.4,
},
yaxis: {
min: 0,
max: 5,
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#cfcfcf'
}, {
from: 2,
to: 3,
color: '#f58e49'
}, {
from: 3,
to: 20,
color: '#e01a1a'
}, {
from: 21,
to: Infinity,
color: '#9b0909'
}]
},
},
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let val = series[seriesIndex][dataPointIndex];
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let ago = `${(dataPointIndex-12) * -1} hours ago`
if ((dataPointIndex-12) * -1 === 0) {
ago = `Current hour`
}
return `<div class="chart_list_tooltip">${val-1} Failures<br>${dt}</div>`
},
fixed: {
enabled: true,
position: 'topLeft',
offsetX: 0,
offsetY: 0,
},
x: {
formatter: (value) => { return value },
},
y: {
show: false
},
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
}
},
mounted() {
this.loadFailures()
},
watch: {
timeframe(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
}
},
methods: {
convertChartData(data) {
if (!data) {
return []
}
let arr = []
data.forEach((d, k) => {
arr.push({
x: d.timeframe,
y: d.amount+1,
})
})
return arr
},
async loadFailures() {
this.loaded = false
let start = 43200
let group = "12h"
if (this.timeframe === "3h") {
start = 10800
group = "5m"
} else if (this.timeframe === "12h") {
start = 43200
group = "1h"
} else if (this.timeframe === "24h") {
start = 86400
group = "2h"
} else if (this.timeframe === "7d") {
start = 86400 * 7
group = "24h"
}
const startTime = this.beginningOf("day", this.nowSubtract(start))
const endTime = this.endOf("day", this.now())
const data = await Api.service_failures_data(this.service.id, this.toUnix(startTime), this.toUnix(endTime), group, true)
this.loaded = true
this.data = [{data: this.convertChartData(data)}]
}
},
}
</script>

View File

@ -2,15 +2,15 @@
<div class="row stats_area mt-5 mb-4">
<div class="col-4">
<span class="font-5 d-block font-weight-bold">{{humanTime(service.avg_response)}}</span>
<span class="font-1 subtitle">{{$t('service.average')}}</span>
<span class="font-1 subtitle">{{$t('average_response')}}</span>
</div>
<div class="col-4">
<span class="font-5 d-block font-weight-bold">{{service.online_24_hours}} %</span>
<span class="font-1 subtitle">{{$t('service.last_uptime', [24, $tc('hour', 24)])}}</span>
<span class="font-1 subtitle">{{$t('last_uptime', [24, $tc('hour', 24)])}}</span>
</div>
<div class="col-4">
<span class="font-5 d-block font-weight-bold">{{service.online_7_days}} %</span>
<span class="font-1 subtitle">{{$t('service.last_uptime', [7, $tc('day', 7)])}}</span>
<span class="font-1 subtitle">{{$t('last_uptime', [7, $tc('day', 7)])}}</span>
</div>
</div>
</template>

View File

@ -16,19 +16,15 @@
<div class="col-12 alert alert-light">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-12 col-md-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
<div class="col-12 col-md-5">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<div class="col-12 col-md-5">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block mt-2">Save Checkin</button>
</div>
@ -54,7 +50,6 @@
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: this.service.id
}
}

View File

@ -1,64 +1,74 @@
<template>
<form @submit.prevent="saveSettings">
<div class="form-group">
<label>{{ $t('settings.name') }}</label>
<input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime" id="project">
<div class="card">
<div class="card-header">Statping Settings</div>
<div class="card-body">
<div class="form-group">
<label>{{ $t('project_name') }}</label>
<input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime" id="project">
</div>
<div class="form-group">
<label>{{ $t('description') }}</label>
<input v-model="core.description" type="text" class="form-control" placeholder="Great Uptime" id="description">
</div>
<div class="form-group row">
<div class="col-8 col-sm-9">
<label>{{ $t('domain') }}</label>
<input v-model="core.domain" type="url" class="form-control" id="domain">
</div>
<div class="col-4 col-sm-3 mt-sm-1 mt-0">
<label class="d-inline d-sm-none">{{$t('enable_cdn')}}</label>
<label class="d-none d-sm-block">{{$t('enable_cdn')}}</label>
<span @click="core.using_cdn = !!core.using_cdn" class="switch" id="using_cdn">
<input v-model="core.using_cdn" type="checkbox" name="using_cdn" class="switch" id="switch-normal" :checked="core.using_cdn">
<label for="switch-normal"></label>
</span>
</div>
</div>
<div class="form-group">
<label>{{ $t('footer') }}</label>
<textarea v-model="core.footer" rows="4" class="form-control" id="footer">{{core.footer}}</textarea>
<small class="form-text text-muted">{{ $t('footer_notes') }}</small>
</div>
<div class="form-group">
<label>{{ $t('language') }}</label>
<select v-model="core.language" class="form-control">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="ru">Russian</option>
<option value="de">German</option>
<option value="ja">Japanese</option>
<option value="it">Italian</option>
<option value="ko">Korean</option>
<option value="zh">Chinese</option>
</select>
</div>
<div class="form-group row mt-3">
<label class="col-sm-10 col-form-label">{{ $t('send_reports') }}</label>
<div class="col-sm-2 float-right">
<span @click="core.allow_reports = !!core.allow_reports" class="switch" id="allow_report">
<input v-model="core.allow_reports" type="checkbox" name="allow_report" class="switch" id="switch_allow_report" :checked="core.allow_reports">
<label for="switch_allow_report"></label>
</span>
</div>
<div class="col-12">
<small>{{ $t('send_reports_desc') }}</small>
</div>
</div>
</div>
<div class="form-group">
<label>{{ $t('settings.description') }}</label>
<input v-model="core.description" type="text" class="form-control" placeholder="Great Uptime" id="description">
<div class="card-footer">
<button @click.prevent="saveSettings" id="save_core" type="submit" class="btn btn-primary btn-block" v-bind:disabled="loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{ $t('save_settings') }}
</button>
</div>
<div class="form-group row">
<div class="col-8 col-sm-9">
<label>{{ $t('domain') }}</label>
<input v-model="core.domain" type="url" class="form-control" id="domain">
</div>
<div class="col-4 col-sm-3 mt-sm-1 mt-0">
<label class="d-inline d-sm-none">Enable CDN</label>
<label class="d-none d-sm-block">Enable CDN</label>
<span @click="core.using_cdn = !!core.using_cdn" class="switch" id="using_cdn">
<input v-model="core.using_cdn" type="checkbox" name="using_cdn" class="switch" id="switch-normal" :checked="core.using_cdn">
<label for="switch-normal"></label>
</span>
</div>
</div>
<div class="form-group">
<label>{{ $t('settings.footer') }}</label>
<textarea v-model="core.footer" rows="4" class="form-control" id="footer">{{core.footer}}</textarea>
<small class="form-text text-muted">{{ $t('settings.footer_notes') }}</small>
</div>
<div class="form-group">
<label>{{ $t('setup.language') }}</label>
<select v-model="core.language" class="form-control">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="ru">Russian</option>
<option value="de">German</option>
</select>
</div>
<div class="form-group row mt-3">
<label class="col-sm-10 col-form-label">{{ $t('settings.error_reporting') }}</label>
<div class="col-sm-2 float-right">
<span @click="core.allow_reports = !!core.allow_reports" class="switch" id="allow_report">
<input v-model="core.allow_reports" type="checkbox" name="allow_report" class="switch" id="switch_allow_report" :checked="core.allow_reports">
<label for="switch_allow_report"></label>
</span>
</div>
<div class="col-12">
<small>{{ $t('settings.error_reporting_notes') }}</small>
</div>
</div>
<button @click.prevent="saveSettings" id="save_core" type="submit" class="btn btn-primary btn-block mt-3" v-bind:disabled="loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{ $t('settings.save') }}
</button>
</div>
</form>
</template>
@ -82,8 +92,8 @@
this.loading = true
const c = this.core
await Api.core_save(c)
const core = await Api.core()
this.$store.commit('setCore', core)
this.$store.commit('setCore', c)
this.$i18n.locale = c.language || "en";
this.loading = false
},
selectAll() {

View File

@ -1,6 +1,6 @@
<template>
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card-header">{{group.id ? `Update ${group.name}` : "Create Group"}}
<div class="card contain-card mb-3">
<div class="card-header">{{group.id ? `${$t('update')} ${group.name}` : $t('group_create')}}
<transition name="slide-fade">
<button @click="removeEdit" v-if="group.id" class="btn float-right btn-danger btn-sm">
{{ $t('close') }}
@ -10,24 +10,24 @@
<form @submit.prevent="saveGroup">
<div class="form-group row">
<label for="title" class="col-sm-4 col-form-label">{{ $t('dashboard.group') }} {{ $t('dashboard.name') }}</label>
<label for="title" class="col-sm-4 col-form-label">{{ $t('group') }} {{ $t('name') }}</label>
<div class="col-sm-8">
<input v-model="group.name" type="text" class="form-control" id="title" placeholder="Group Name" required>
</div>
</div>
<div class="form-group row">
<label for="switch-group-public" class="col-sm-4 col-form-label text-capitalize">{{ $t('public') }} {{ $t('dashboard.group') }}</label>
<label for="switch-group-public" class="col-sm-4 col-form-label text-capitalize">{{ $t('public') }} {{ $t('group') }}</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="group.public = !!group.public" class="switch float-left">
<input v-model="group.public" type="checkbox" class="switch" id="switch-group-public" :checked="group.public">
<label for="switch-group-public">Show group services to the public</label>
<label for="switch-group-public">{{$t('group_public_desc')}}</label>
</span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="saveGroup" type="submit" :disabled="loading || group.name === ''" class="btn btn-block" :class="{'btn-primary': !group.id, 'btn-secondary': group.id}">
{{loading ? "Loading..." : group.id ? "Update Group" : "Create Group"}}
{{loading ? "Loading..." : group.id ? $t('group_update') : $t('group_create')}}
</button>
</div>
</div>

View File

@ -1,44 +0,0 @@
<template>
<div>
</div>
</template>
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import FormIncidentUpdates from './IncidentUpdates';
export default {
name: 'FormIncident',
components: {
FormIncidentUpdates
},
props: {
service: {
type: Object
}
},
data () {
return {
incident: {
title: "",
description: "",
service: this.service.id,
},
incidents: [],
}
},
async mounted () {
await this.loadIncidents()
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,24 +1,16 @@
<template>
<div class="card-body bg-light pt-3">
<div class="card-body pt-3">
<div v-if="updates.length===0" class="alert alert-link text-danger">
No updates found, create a new Incident Update below.
</div>
<div v-for="update in updates" :key="update.id">
<div class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
</div>
<div v-for="update in updates.reverse()" :key="update.id">
<IncidentUpdate :update="update" :onUpdate="loadUpdates" :admin="true"/>
</div>
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-3">
<div class="col-12 col-md-3 mb-3 mb-md-0">
<select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option>
<option value="Update">Update</option>
@ -26,11 +18,11 @@
<option value="Resolved">Resolved</option>
</select>
</div>
<div class="col-7">
<input v-model="incident_update.message" rows="5" name="description" class="form-control" id="message" required>
<div class="col-12 col-md-7 mb-3 mb-md-0">
<input v-model="incident_update.message" name="description" class="form-control" id="message" required>
</div>
<div class="col-2">
<div class="col-12 col-md-2">
<button @click.prevent="createIncidentUpdate"
:disabled="!incident_update.message"
type="submit" class="btn btn-block btn-primary">
@ -44,12 +36,11 @@
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const IncidentUpdate = () => import(/* webpackChunkName: "index" */ "@/components/Elements/IncidentUpdate");
export default {
name: 'FormIncidentUpdates',
components: {},
components: {IncidentUpdate},
props: {
incident: {
type: Object,
@ -58,7 +49,7 @@
},
data () {
return {
updates: [],
updates: null,
incident_update: {
incident: this.incident.id,
message: "",
@ -72,15 +63,6 @@
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.updates = this.updates.filter(obj => obj.id !== update.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
//await this.loadUpdates()
}
},
async createIncidentUpdate() {
this.res = await Api.incident_update_create(this.incident_update)
if (this.res.status === "success") {

View File

@ -2,30 +2,30 @@
<div>
<form @submit.prevent="login" autocomplete="on">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">{{$t('username')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="text" v-model="username" name="username" class="form-control" id="username" placeholder="Username" autocorrect="off" autocapitalize="none">
<label for="username" class="col-4 col-form-label">{{$t('username')}}</label>
<div class="col-8">
<input @keyup="checkForm" type="text" v-model="username" autocomplete="username" name="username" class="form-control" id="username" placeholder="admin" autocorrect="off" autocapitalize="none">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">{{$t('password')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="password" v-model="password" name="password" class="form-control" id="password" placeholder="Password">
<label for="password" class="col-4 col-form-label">{{$t('password')}}</label>
<div class="col-8">
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="************">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<div v-if="error" class="alert alert-danger" role="alert">
{{$t('dashboard.wrong_login')}}
{{$t('wrong_login')}}
</div>
<button @click.prevent="login" type="submit" class="btn btn-block mb-3 btn-primary" :disabled="disabled || loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? $t('dashboard.loading') : $t('dashboard.sign_in')}}
<button @click.prevent="login" type="submit" class="btn btn-block btn-primary" :disabled="disabled || loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? $t('loading') : $t('sign_in')}}
</button>
</div>
</div>
</form>
<a v-if="oauth && oauth.gh_client_id" @click.prevent="GHlogin" href="#" class="btn btn-block btn-outline-dark">
<a v-if="oauth && oauth.gh_client_id" @click.prevent="GHlogin" href="#" class="mt-4 btn btn-block btn-outline-dark">
<font-awesome-icon :icon="['fab', 'github']" /> Login with Github
</a>
@ -59,17 +59,20 @@
},
data() {
return {
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
google_scope: "https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email",
slack_scope: "identity.email,identity.basic"
}
},
methods: {
mounted() {
this.$cookies.remove("statping_auth")
},
methods: {
checkForm() {
if (!this.username || !this.password) {
this.disabled = true
@ -84,24 +87,39 @@
if (auth.error) {
this.error = true
} else if (auth.token) {
// this.$cookies.set("statping_auth", auth.token)
this.$cookies.set("statping_auth", auth.token)
await this.$store.dispatch('loadAdmin')
this.$store.commit('setAdmin', auth.admin)
this.$store.commit('setLoggedIn', true)
this.$router.push('/dashboard')
}
this.loading = false
},
encode(val) {
return encodeURI(val)
},
custom_scopes() {
let scopes = []
if (this.oauth.custom_open_id) {
scopes.push("openid")
}
scopes.push(this.oauth.custom_scopes.split(","))
if (scopes.length !== 0) {
return "&scopes="+scopes.join(",")
}
return ""
},
GHlogin() {
window.location = `https://github.com/login/oauth/authorize?client_id=${this.oauth.gh_client_id}&redirect_uri=${this.core.domain}/oauth/github&scope=user,repo`
window.location = `https://github.com/login/oauth/authorize?client_id=${this.oauth.gh_client_id}&redirect_uri=${this.encode(this.core.domain+"/oauth/github")}&scope=user,repo`
},
Slacklogin() {
window.location = `https://slack.com/oauth/authorize?client_id=${this.oauth.slack_client_id}&redirect_uri=${this.core.domain}/oauth/slack&scope=identity.basic`
window.location = `https://slack.com/oauth/authorize?client_id=${this.oauth.slack_client_id}&redirect_uri=${this.encode(this.core.domain+"/oauth/slack")}&scope=identity.basic`
},
Googlelogin() {
window.location = `https://accounts.google.com/signin/oauth?client_id=${this.oauth.google_client_id}&redirect_uri=${this.core.domain}/oauth/google&response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email`
window.location = `https://accounts.google.com/signin/oauth?client_id=${this.oauth.google_client_id}&redirect_uri=${this.encode(this.core.domain+"/oauth/google")}&response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email`
},
Customlogin() {
window.location = `${this.oauth.custom_endpoint_auth}?client_id=${this.oauth.custom_client_id}&redirect_uri=${this.core.domain}/oauth/custom${this.oauth.custom_scopes !== "" ? "&scope="+this.oauth.custom_scopes : "" }`
window.location = `${this.oauth.custom_endpoint_auth}?client_id=${this.oauth.custom_client_id}&redirect_uri=${this.encode(this.core.domain+"/oauth/custom")}&response_type=code${this.custom_scopes()}`
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Announcement"}}
<div class="card contain-card mb-5">
<div class="card-header">{{message.id ? `${$t('update')} ${message.title}` : $t('message_create')}}
<transition name="slide-fade">
<button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">
{{ $t('close') }}
@ -11,75 +11,75 @@
<div class="card-body">
<form @submit="saveMessage">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('dashboard.title') }}</label>
<label class="col-sm-4 col-form-label">{{ $t('title') }}</label>
<div class="col-sm-8">
<input v-model="message.title" type="text" name="title" class="form-control" id="title" placeholder="Announcement Title" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Description</label>
<label class="col-sm-4 col-form-label">{{ $t('description') }}</label>
<div class="col-sm-8">
<textarea v-model="message.description" rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Service</label>
<label class="col-sm-4 col-form-label">{{ $t('service') }}</label>
<div class="col-sm-8">
<select v-model="message.service" name="service_id" class="form-control">
<option :value="0">Global Announcement</option>
<option v-bind:value="0">{{ $t('global_announcement') }}</option>
<option v-for="service in $store.getters.services" :value="service.id" v-bind:key="service.id" >{{service.name}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Announcement Date Range</label>
<label class="col-sm-4 col-form-label">{{ $t('announcement_date') }}</label>
<div class="col-sm-4">
<flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
</div>
<div class="col-sm-4">
<div class="col-sm-4 mt-3 mt-md-0">
<flatPickr v-model="message.end_on" @on-change="endChange" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required />
</div>
</div>
<div v-show="this.service === null" class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<label for="service_id" class="col-sm-4 col-form-label">{{ $t('service') }}</label>
<div class="col-sm-8">
<select v-model="message.service" class="form-control" name="service" id="service_id">
<option :value="0">Global Message</option>
<option v-bind:value="0">{{ $t('global_announcement') }}</option>
<option v-for="service in $store.getters.services" :value="service.id" v-bind:key="service.id">{{service.name}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notify Users</label>
<label for="notify_method" class="col-sm-4 col-form-label">{{ $t('notify_users') }}</label>
<div class="col-sm-8">
<span @click="message.notify = !!message.notify" class="switch">
<input v-model="message.notify" type="checkbox" class="switch" id="switch-normal">
<label for="switch-normal">Notify Users Before Scheduled Time</label>
<label for="switch-normal">{{ $t('notify_desc') }}</label>
</span>
</div>
</div>
<div v-if="message.notify" class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<label for="notify_method" class="col-sm-4 col-form-label">{{ $t('notify_method') }}</label>
<div class="col-sm-8">
<input v-model="message.notify_method" type="text" name="notify_method" class="form-control" id="notify_method" value="" placeholder="email">
</div>
</div>
<div v-if="message.notify" class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<label for="notify_before" class="col-sm-4 col-form-label">{{ $t('notify_before') }}</label>
<div class="col-sm-8">
<div class="form-inline">
<input v-model="message.notify_before" type="number" name="notify_before" class="col-4 form-control" id="notify_before">
<input v-model.number="message.notify_before" type="number" name="notify_before" class="col-4 form-control" id="notify_before">
<select v-model="message.notify_before_scale" class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute">Minutes</option>
<option value="hour">Hours</option>
<option value="day">Days</option>
<option value="minute">{{ $t('minutes') }}</option>
<option value="hour">{{ $t('hours') }}</option>
<option value="day">{{ $t('days') }}</option>
</select>
</div>
</div>
@ -90,7 +90,7 @@
<button @click="saveMessage"
:disabled="!message.title || !message.description"
type="submit" class="btn btn-block" :class="{'btn-primary': !message.id, 'btn-secondary': message.id}">
{{message.id ? "Edit Message" : "Create Message"}}
{{message.id ? $t('message_edit') : $t('message_create')}}
</button>
</div>
</div>
@ -130,6 +130,7 @@
start_on: new Date(),
end_on: new Date(),
service_id: 0,
service: 0,
notify_method: "",
notify: false,
notify_before: 0,

View File

@ -1,7 +1,7 @@
<template>
<div>
<form @submit.prevent="saveNotifier">
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card contain-card mb-3">
<div class="card-header text-capitalize">
{{notifier.title}}
<span @click="enableToggle" class="switch switch-sm switch-rd-gr float-right">
@ -14,10 +14,10 @@
<div v-if="notifier.method==='mobile'">
<div class="form-group row mt-3">
<label for="domain" class="col-sm-4 col-form-label">Statping Domain</label>
<label for="statping_domain" class="col-sm-4 col-form-label">Statping Domain</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="domain" readonly>
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="statping_domain" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy($store.getters.core.domain)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
@ -69,7 +69,7 @@
</div>
</div>
<div v-if="notifier.data_type" class="card text-black-50 bg-white mb-3">
<div v-if="notifier.data_type" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Outgoing Request
@ -103,7 +103,7 @@
</form>
<div v-if="error || success" class="card text-black-50 bg-white mb-3">
<div v-if="error || success" class="card mb-3">
<div class="card-body">
<div v-if="error && !success" class="alert alert-danger col-12" role="alert">
@ -119,7 +119,7 @@
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
@ -128,14 +128,39 @@
</button>
</div>
<div class="col-12 col-md-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier('success')" :disabled="loadingTest" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<button @click.prevent="testNotifier('success')" :disabled="loadingTest" class="btn btn-secondary btn-block text-capitalize test-notifier">
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Success"}}</button>
</div>
<div class="col-12 col-md-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier('failure')" :disabled="loadingTest" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<button @click.prevent="testNotifier('failure')" :disabled="loadingTest" class="btn btn-secondary btn-block text-capitalize test-notifier">
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Failure"}}</button>
</div>
</div>
</div>
</div>
<div v-if="notifier.logs" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded_logs = !expanded_logs" :icon="expanded_logs ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Logs
<span class="badge badge-info float-right text-uppercase mt-1">{{notifier.logs.length}}</span>
</div>
<div class="card-body" :class="{'d-none': !expanded_logs}">
<div v-for="(log, i) in notifier.logs.reverse()" class="alert" :class="{'alert-danger': log.error, 'alert-dark': !log.success && !log.error, 'alert-success': log.success && !log.error}">
<span class="d-block">
Service {{log.service}}
{{log.success ? "Success Triggered" : "Failure Triggered"}}
</span>
<div v-if="log.message !== ''" class="bg-white p-3 small mt-2">
<code>{{log.message}}</code>
</div>
<div class="row mt-2">
<span class="col-6 small">{{niceDate(log.created_at)}}</span>
</div>
</div>
</div>
</div>
@ -148,16 +173,19 @@
</template>
<script>
import Api from "../API";
import Api from "../API";
/* webpackChunkName: "codemirror" */
import {codemirror} from 'vue-codemirror'
/* webpackChunkName: "codemirror" */
import 'codemirror/mode/javascript/javascript.js'
/* webpackChunkName: "codemirror" */
import 'codemirror/lib/codemirror.css'
/* webpackChunkName: "codemirror" */
import 'codemirror/theme/neat.css'
/* webpackChunkName: "codemirror" */
import '../codemirror_json'
const beautify = require('js-beautify').js
// require component
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/neat.css'
import '../codemirror_json'
const beautify = require('js-beautify').js
export default {
name: 'Notifier',
@ -183,6 +211,7 @@ export default {
success: false,
saved: false,
expanded: false,
expanded_logs: false,
success_data: null,
failure_data: null,
form: {},
@ -300,10 +329,3 @@ export default {
}
}
</script>
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;
font-size: 9pt;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<form @submit.prevent="saveOAuth">
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
Internal Login
<span @click="local_enabled = !!local_enabled" class="switch switch-sm switch-rd-gr float-right">
@ -12,7 +12,7 @@
Use Statping's default authentication to allow users you've created to login.
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded.github = !expanded.github" :icon="expanded.github ? 'minus' : 'plus'" class="mr-2 pointer"/>
Github Settings
@ -50,12 +50,6 @@
<small>Optional comma delimited list of Github Organizations</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Github Login</label>
<div class="col-md-8 col-xs-12 mt-1">
</div>
</div>
<div class="form-group row">
<label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">
@ -69,7 +63,7 @@
</div>
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.google = !expanded.google" :icon="expanded.google ? 'minus' : 'plus'" class="mr-2 pointer"/>
Google Settings
@ -113,7 +107,7 @@
</div>
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.slack = !expanded.slack" :icon="expanded.slack ? 'minus' : 'plus'" class="mr-2 pointer"/>
Slack Settings
@ -165,7 +159,7 @@
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card mb-3">
<div class="card-header">
<font-awesome-icon @click="expanded.custom = !expanded.custom" :icon="expanded.custom ? 'minus' : 'plus'" class="mr-2 pointer"/>
Custom oAuth Settings
@ -174,8 +168,8 @@
<label for="switch-custom-oauth" class="mb-0"> </label>
</span>
</div>
<div class="card-body" :class="{'d-none': !expanded.custom}">
<div class="form-group row mt-3">
<div class="card-body" :class="{'d-none': !expanded.custom || !custom_enabled}">
<div class="form-group row">
<label for="custom_name" class="col-sm-4 col-form-label">Custom Name</label>
<div class="col-sm-8">
<input v-model="oauth.custom_name" type="text" class="form-control" id="custom_name" required>
@ -205,13 +199,24 @@
<input v-model="oauth.custom_endpoint_token" type="text" class="form-control" id="custom_endpoint_token" required>
</div>
</div>
<div class="form-group row">
<label for="custom_scopes" class="col-sm-4 col-form-label">Scopes</label>
<div class="col-sm-8">
<input v-model="oauth.custom_scopes" type="text" class="form-control" id="custom_scopes">
<small>Optional comma delimited list of oauth scopes</small>
</div>
<div class="form-group row">
<label for="custom_scopes" class="col-sm-4 col-form-label">Scopes</label>
<div class="col-sm-8">
<input v-model="oauth.custom_scopes" type="text" class="form-control" id="custom_scopes">
<small>Optional comma delimited list of oauth scopes</small>
</div>
</div>
<div class="form-group row">
<label for="custom_scopes" class="col-sm-4 col-form-label">Open ID</label>
<div class="col-sm-8">
<span @click="oauth.custom_open_id = !!oauth.custom_open_id" class="switch switch-rd-gr float-right">
<input v-model="oauth.custom_open_id" type="checkbox" id="switch-custom-openid" :checked="oauth.custom_open_id">
<label for="switch-custom-openid" class="mb-0"> </label>
</span>
<small>Enable if provider is OpenID</small>
</div>
</div>
<div class="form-group row">
<label for="slack_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">
@ -277,6 +282,7 @@
custom_endpoint_auth: "",
custom_endpoint_token: "",
custom_scopes: "",
custom_open_id: false,
}
}
},

View File

@ -1,24 +1,25 @@
<template>
<form v-if="service.type" @submit.prevent="saveService">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{ $t('service.info') }}</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('service_info') }}</div>
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('service.name') }}</label>
<div class="col-sm-8">
<input v-model="service.name" @input="updatePermalink" id="name" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('service_name') }}</label>
<div class="col-sm-8">
<input v-model="service.name" @input="updatePermalink" id="name" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">{{ $t('service.type') }}</label>
<label for="service_type" class="col-sm-4 col-form-label">{{ $t('service_type') }}</label>
<div class="col-sm-8">
<select v-model="service.type" class="form-control" id="service_type">
<option value="http">HTTP Service</option>
<option value="tcp">TCP Service</option>
<option value="udp">UDP Service</option>
<option value="http">HTTP {{ $t('service') }}</option>
<option value="tcp">TCP {{ $t('service') }}</option>
<option value="udp">UDP {{ $t('service') }}</option>
<option value="icmp">ICMP Ping</option>
<option value="grpc">gRPC Service</option>
<option value="grpc">gRPC {{ $t('service') }}</option>
<option value="static">Static {{ $t('service') }}</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div>
@ -34,7 +35,7 @@
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Permalink URL</label>
<label class="col-sm-4 col-form-label">{{ $t('permalink') }}</label>
<div class="col-sm-8">
<input v-model="service.permalink" type="text" name="permalink" class="form-control" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
@ -42,8 +43,8 @@
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Public Service</label>
<div class="col-8 mt-1">
<label class="col-sm-4 col-form-label">{{ $t('service_public') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2">
<span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
<label v-if="service.public" for="switch-public">This service will be visible for everyone</label>
@ -52,29 +53,31 @@
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval</label>
<div v-if="service.type !== 'static'" class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">{{ $t('check_interval') }}</label>
<div class="col-sm-6">
<span class="slider-info">{{secondsHumanize(service.check_interval)}}</span>
<input v-model="service.check_interval" type="range" class="slider" id="service_interval" min="1" max="1800" :step="1">
<small id="interval" class="form-text text-muted">Interval to check your service state</small>
</div>
<div class="col-sm-2">
<input v-model="service.check_interval" type="text" name="check_interval" class="form-control">
<input v-model="service.check_interval" type="number" name="check_interval" class="form-control">
</div>
</div>
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div v-if="service.type !== 'static'" class="card contain-card mb-4">
<div class="card-header">Request Details</div>
<div class="card-body">
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Service Endpoint {{service.type === 'http' ? "(URL)" : "(Domain)"}}</label>
<label for="service_url" class="col-sm-4 col-form-label">
{{ $t('service_endpoint') }} {{service.type === 'http' ? "(URL)" : "(Domain)"}}
</label>
<div class="col-sm-8">
<input v-model="service.domain" type="text" class="form-control" id="service_url" :placeholder="service.type === 'http' ? 'https://google.com' : '192.168.1.1'" required autocapitalize="none" spellcheck="false">
<input v-model="service.domain" type="url" class="form-control" id="service_url" :placeholder="service.type === 'http' ? 'https://google.com' : '192.168.1.1'" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this address</small>
</div>
</div>
@ -87,7 +90,7 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Service Check Type</label>
<label class="col-sm-4 col-form-label">{{ $t('service_check') }}</label>
<div class="col-sm-8">
<select v-model="service.method" name="method" class="form-control">
<option value="GET" >GET</option>
@ -101,7 +104,7 @@
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Request Timeout</label>
<label class="col-sm-4 col-form-label">{{ $t('service_timeout') }}</label>
<div class="col-sm-6">
<span v-if="service.timeout >= 0" class="slider-info">{{secondsHumanize(service.timeout)}}</span>
<input v-model="service.timeout" type="range" id="timeout" name="timeout" class="slider" min="1" max="180">
@ -109,7 +112,7 @@
</div>
<div class="col-sm-2">
<input v-model="service.timeout" type="text" name="service_timeout" class="form-control">
<input v-model="service.timeout" type="number" name="service_timeout" class="form-control">
</div>
</div>
@ -129,14 +132,14 @@
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<label class="col-sm-4 col-form-label">{{ $t('expected_resp') }} (Regex)</label>
<div class="col-sm-8">
<textarea v-model="service.expected" class="form-control" rows="3" autocapitalize="none" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'></textarea>
<small class="form-text text-muted">You can use plain text or insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<label for="service_response_code" class="col-sm-4 col-form-label">{{ $t('expected_code') }}</label>
<div class="col-sm-8">
<input v-model="service.expected_status" type="number" name="expected_status" class="form-control" placeholder="200" id="service_response_code">
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
@ -144,8 +147,8 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">{{ $t('follow_redir') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.redirect = !!service.redirect" class="switch float-left">
<input v-model="service.redirect" type="checkbox" name="redirect-option" class="switch" id="switch-redirect" v-bind:checked="service.redirect">
<label for="switch-redirect">Follow HTTP Redirects if server attempts</label>
@ -154,8 +157,8 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">{{ $t('verify_ssl') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
<label for="switch-verify-ssl" v-if="service.verify_ssl">Verify SSL Certificate for this service</label>
@ -165,8 +168,8 @@
</div>
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Use TLS Certificate</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">{{ $t('tls_cert') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="use_tls = !!use_tls" class="switch float-left">
<input v-model="use_tls" type="checkbox" name="verify_ssl-option" class="switch" id="switch-use-tls" v-bind:checked="use_tls">
<label for="switch-use-tls" v-if="use_tls">Custom TLS Certificates for mTLS services</label>
@ -202,13 +205,13 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Notification Options</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('notification_opts') }}</div>
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-8 mt-1">
<label class="col-sm-4 col-form-label">{{ $t('notifications_enable') }}</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
<label for="switch-notifications">Allow notifications to be sent for this service</label>
@ -216,7 +219,7 @@
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify After Failures</label>
<label class="col-sm-4 col-form-label">{{ $t('notify_after') }}</label>
<div class="col-sm-8">
<span class="slider-info">{{service.notify_after === 0 ? "First Failure" : service.notify_after+' Failures'}}</span>
<input v-model="service.notify_after" type="range" name="notify_after" class="slider" id="notify_after" min="0" max="20">
@ -224,8 +227,8 @@
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<label class="col-sm-4 col-form-label">{{ $t('notify_all') }}</label>
<div class="col-12 col-md-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label v-if="service.notify_all_changes" for="notify_all">Continuously send notifications when service is failing.</label>
@ -240,7 +243,7 @@
<div class="form-group row">
<div class="col-12">
<button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
{{service.id ? "Update Service" : "Create Service"}}
{{service.id ? $t('service_update') : $t('service_create')}}
</button>
</div>
</div>

View File

@ -1,25 +1,29 @@
<template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img alt="Statping Setup" class="col-12 mt-5 mt-md-0" style="max-width:680px" src="banner.png">
<div class="col-12 col-md-6 offset-md-3 mb-4">
<img alt="Statping Setup" class="img-fluid mt-5 mt-md-0" src="banner.png">
</div>
<div class="col-12">
<form @submit.prevent="saveSetup">
<div class="row">
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.language') }}</label>
<label class="text-capitalize">{{ $t('language') }}</label>
<select @change="changeLanguages" v-model="setup.language" id="language" class="form-control">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="ru">Russian</option>
<option value="de">German</option>
<option value="ja">Japanese</option>
<option value="ko">Korean</option>
<option value="it">Italian</option>
<option value="zh">Chinese</option>
</select>
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.connection') }}</label>
<label class="text-capitalize">{{ $t('db_connection') }}</label>
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
<option value="sqlite">SQLite</option>
<option value="postgres">Postgres</option>
@ -27,36 +31,36 @@
</select>
</div>
<div class="row">
<div class="col-6">
<div class="col-7 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('setup.host') }}</label>
<label class="text-capitalize">{{ $t('db_host') }}</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
</div>
<div class="col-6">
<div class="col-5 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('port') }}</label>
<label class="text-capitalize">{{ $t('db_port') }}</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="number" class="form-control" placeholder="5432">
</div>
</div>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('username') }}</label>
<label class="text-capitalize">{{ $t('db_username') }}</label>
<input @keyup="canSubmit" v-model="setup.db_user" id="db_user" type="text" class="form-control" placeholder="root">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_password" class="text-capitalize">{{ $t('password') }}</label>
<label for="db_password" class="text-capitalize">{{ $t('db_password') }}</label>
<input @keyup="canSubmit" v-model="setup.db_password" id="db_password" type="password" class="form-control" placeholder="password123">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_database" class="text-capitalize">{{ $t('setup.database') }}</label>
<label for="db_database" class="text-capitalize">{{ $t('db_database') }}</label>
<input @keyup="canSubmit" v-model="setup.db_database" id="db_database" type="text" class="form-control" placeholder="Database name">
</div>
<div class="form-group mt-3">
<div class="row">
<div class="col-9">
<span class="text-left text-capitalize">{{ $t('setup.send_reports') }}</span>
<span class="text-left text-capitalize">{{ $t('send_reports') }}</span>
</div>
<div class="col-3 text-right">
<span @click="setup.send_reports = !!setup.send_reports" class="switch">
@ -65,40 +69,39 @@
</span>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.project_name') }}</label>
<label class="text-capitalize">{{ $t('project_name') }}</label>
<input @keyup="canSubmit" v-model="setup.project" id="project" type="text" class="form-control" placeholder="Work Servers" required>
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.project_description') }}</label>
<label class="text-capitalize">{{ $t('description') }}</label>
<input @keyup="canSubmit" v-model="setup.description" id="description" type="text" class="form-control" placeholder="Monitors all of my work services">
</div>
<div class="form-group">
<label class="text-capitalize" for="domain">{{ $t('setup.domain') }}</label>
<label class="text-capitalize" for="domain">{{ $t('domain') }}</label>
<input @keyup="canSubmit" v-model="setup.domain" type="text" class="form-control" id="domain" required>
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.username') }}</label>
<label class="text-capitalize">{{ $t('username') }}</label>
<input @keyup="canSubmit" v-model="setup.username" id="username" type="text" class="form-control" placeholder="admin" required>
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.password') }}</label>
<label class="text-capitalize">{{ $t('password') }}</label>
<input @keyup="canSubmit" v-model="setup.password" id="password" type="password" class="form-control" placeholder="password" required>
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.password_confirm') }}</label>
<label class="text-capitalize">{{ $t('confirm_password') }}</label>
<input @keyup="canSubmit" v-model="setup.confirm_password" id="password_confirm" type="password" class="form-control" placeholder="password" required>
<span v-if="passnomatch" class="small text-danger">Both passwords should match</span>
</div>
@ -110,14 +113,14 @@
<input @keyup="canSubmit" v-model="setup.email" id="email" type="text" class="form-control" placeholder="myemail@domain.com">
</div>
<div class="col-4 text-right">
<label class="d-none d-sm-block text-capitalize text-capitalize">{{ $t('setup.newsletter') }}</label>
<label class="d-none d-sm-block text-capitalize text-capitalize">{{ $t('newsletter') }}</label>
<span @click="setup.newsletter = !!setup.newsletter" class="switch">
<input v-model="setup.newsletter" type="checkbox" name="send_newsletter" class="switch" id="send_newsletter" :checked="setup.newsletter">
<label for="send_newsletter"></label>
</span>
</div>
</div>
<small>{{ $t('setup.newsletter_note') }}</small>
<small>{{ $t('newsletter_note') }}</small>
</div>
</div>
@ -125,9 +128,11 @@
{{error}}
</div>
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? "Loading..." : "Save Settings"}}
</button>
<div class="col-12">
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? $t('loading') : $t('save_settings')}}
</button>
</div>
</div>
</form>
@ -137,7 +142,6 @@
<script>
import Api from "../API";
import Index from "../pages/Index";
export default {
name: 'Setup',

View File

@ -1,6 +1,6 @@
<template>
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card-header"> {{user.id ? `Update ${user.username}` : "Create User"}}
<div class="card contain-card mb-3">
<div class="card-header"> {{user.id ? `${$t('update')} ${user.username}` : $t('user_create')}}
<transition name="slide-fade">
<button @click.prevent="removeEdit" v-if="user.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button>
</transition>
@ -8,31 +8,31 @@
<div class="card-body">
<form @submit="saveUser">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Username</label>
<label class="col-sm-4 col-form-label">{{$t('username')}}</label>
<div class="col-6 col-md-4">
<input v-model="user.username" type="text" class="form-control" id="username" placeholder="Username" required autocorrect="off" autocapitalize="none" v-bind:readonly="user.id">
</div>
<div class="col-6 col-md-4">
<span id="admin_switch" @click="user.admin = !!user.admin" class="switch">
<input v-model="user.admin" type="checkbox" class="switch" id="user_admin_switch" v-bind:checked="user.admin">
<label for="user_admin_switch">Administrator</label>
<label for="user_admin_switch">{{$t('administrator')}}</label>
</span>
</div>
</div>
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label">Email Address</label>
<label for="email" class="col-sm-4 col-form-label">{{$t('email')}}</label>
<div class="col-sm-8">
<input v-model="user.email" type="email" class="form-control" id="email" placeholder="user@domain.com" required autocapitalize="none" spellcheck="false">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Password</label>
<label class="col-sm-4 col-form-label">{{$t('password')}}</label>
<div class="col-sm-8">
<input v-model="user.password" type="password" id="password" class="form-control" placeholder="Password" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Confirm Password</label>
<label class="col-sm-4 col-form-label">{{$t('confirm_password')}}</label>
<div class="col-sm-8">
<input v-model="user.confirm_password" type="password" id="password_confirm" class="form-control" placeholder="Confirm Password" required>
</div>
@ -54,7 +54,7 @@
class="btn-primary"
:disabled="loading || !user.username || !user.email || !user.password || !user.confirm_password || (user.password !== user.confirm_password)"
:action="saveUser"
:label="user.id ? 'Update User' : 'Create User'"
:label="user.id ? $t('user_update'): $t('user_create')"
/>
</div>
</div>
@ -66,7 +66,7 @@
<script>
import Api from "../API";
import LoadButton from "@/components/Elements/LoadButton";
const LoadButton = () => import(/* webpackChunkName: "index" */ "@/components/Elements/LoadButton");
export default {
name: 'FormUser',

View File

@ -1,7 +1,6 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid';
import {fab} from '@fortawesome/free-brands-svg-icons';
import {far} from '@fortawesome/fontawesome-svg-core';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import Vue from "vue";

142
frontend/src/languages/chinese.js Executable file
View File

@ -0,0 +1,142 @@
const chinese = {
settings: "设置",
dashboard: "仪表板",
services: "服务",
service: "服务",
failures: "失败",
users: "用户",
login: "登录",
logout: "注销",
online: "在线",
offline: "离线",
configs: "配置",
username: "用户名",
password: "密码",
email: "电子邮件",
confirm_password: "确认密码",
uptime: "正常运行时间",
name: "名称",
copy: "复制",
close: "关",
secret: "秘密",
regen_api: "重新生成 API 密钥",
regen_desc: "API 密码用于读取创建更新和删除路由。如果需要,您可以重新生成 API 密钥。",
visibility: "可见性",
group: "小组",
group_create: "创建组",
group_update: "更新组",
group_public_desc: "向公众显示群组服务",
groups: "组",
no_group: "无组",
public: "公共",
private: "私人",
announcements: "公告",
notifiers: "通告程序",
logs: "日志",
help: "帮助",
type: "类型",
edit: "编辑",
update: "更新",
create: "创建",
view: "查看",
save: "保存",
title: "标题",
status: "状态",
begins: "开始",
total_services: "服务总数",
online_services: "在线服务",
request_timeout: "请求超时",
service_never_online: "服务从未在线",
service_online_check: "在线检查",
service_offline_time: "服务已脱机",
days_ago: "天前",
today: "今天",
week: "周",
month: "月份",
day: "日",
hour: "小时",
minute: "分钟",
failures_24_hours: "过去 24 小时失败",
no_services: "您目前没有任何服务!",
theme: "主题",
cache: "高速缓存",
authentication: "身份验证",
import: "导入",
main_settings: "主要设置",
variables: "变量",
docs: "文档",
links: "链接",
changelog: "更改日志",
repo: "存储库",
language: "语言",
db_connection: "数据库连接",
db_host: "数据库主机",
db_port: "数据库端口",
db_username: "数据库用户名",
db_password: "数据库口令",
db_database: "数据库名称",
send_reports: "发送错误报告",
send_reports_desc: "将错误发送到 “状态” 以进行调试",
project_name: "状态页面名称",
description: "说明",
domain: "域",
enable_cdn: "启用 CDN",
newsletter: "新闻通讯",
newsletter_note: "我们只会向您发送重大变更的电子邮件",
loading: "正在加载",
save_settings: "保存设置",
average_response: "平均响应",
last_uptime: "上次正常运行时间",
sign_in: "登录",
last_login: "上次登录",
admin: "管理员",
user: "用户",
failed: "失败",
wrong_login: "用户名或密码不正确",
theme_editor: "主题编辑器",
enable_assets: "启用本地资产",
assets_desc: "通过启用本地资产自定义状态页面设计。这将创建一个包含所有 CSS 的 “资产” 目录。",
assets_btn: "启用本地资产",
assets_loading: "创建资产",
assets_dir: "资产目录",
footer: "页脚",
footer_notes: "您可以在页脚中使用 HTML 标签",
global_announcement: "全球公告",
announcement_date: "公告日期范围",
notify_users: "通知用户",
notify_desc: "在计划时间之前通知用户",
notify_method: "通知方法",
notify_before: "之前通知",
message_create: "创建公告",
message_edit: "编辑公告",
minutes: "分钟",
hours: "小时数",
days: "天数",
user_create: "创建用户",
user_update: "更新用户",
administrator: "管理员",
checkins: "车臣金斯",
incidents: "事件",
service_info: "服务信息",
service_name: "服务名称",
service_type: "服务类型",
permalink: "永久链接 URL",
service_public: "公共服务",
check_interval: "检查间隔",
service_endpoint: "服务终端节点",
service_check: "服务检查类型",
service_timeout: "请求超时",
expected_resp: "预期响应",
expected_code: "预期状态代码",
follow_redir: "跟随重定向",
verify_ssl: "验证 SSL",
tls_cert: "使用 TLS 证书",
notification_opts: "通知选项",
notifications_enable: "启用通知",
notify_after: "故障后通知",
notify_all: "通知所有更改",
service_update: "更新服务",
service_create: "创建服务"
}
export default chinese

View File

@ -0,0 +1,139 @@
key,en
settings,Settings
dashboard,Dashboard
services,Services
service,Service
failures,Failures
users,Users
login,Login
logout,Logout
online,Online
offline,Offline
configs,Configuration
username,Username
password,Password
email,Email
confirm_password,Confirm Password
uptime,Uptime
name,Name
copy,Copy
close,Close
secret,Secret
regen_api,Regenerate API Keys
regen_desc,API Secret is used for read create update and delete routes. You can Regenerate API Keys if you need to.
visibility,Visibility
group,Group
group_create,Create Group
group_update,Update Group
group_public_desc,Show group services to the public
groups,Groups
no_group,No Group
public,Public
private,Private
announcements,Announcements
notifiers,Notifiers
logs,Logs
help,Help
type,Type
edit,Edit
update,Update
create,Create
view,View
save,Save
title,Title
status,Status
begins,Begins
total_services,Total Services
online_services,Online Services
request_timeout,Request Timeout
service_never_online,Service has never been online
service_online_check,Online checked
service_offline_time,Service has been offline for
days_ago,Days Ago
today,Today
week,Week
month,Month
day,Day
hour,Hour
minute,Minute
failures_24_hours,Failures last 24 hours
no_services,You currently don't have any services!
theme,Theme
cache,Cache
authentication,Authentication
import,Import
main_settings,Main Settings
variables,Variables
docs,Documentation
links,Links
changelog,Change Log
repo,Repository
language,Language
db_connection,Database Connection
db_host,Database Host
db_port,Database Port
db_username,Database Username
db_password,Database Password
db_database,Database Name
send_reports,Send Error Reports
send_reports_desc,Send errors to Statping for debugging
project_name,Status Page Name
description,Description
domain,Domain
enable_cdn,Enable CDN
newsletter,Newsletter
newsletter_note,We will only send you an email for major changes
loading,Loading
save_settings,Save Settings
average_response,Average Response
last_uptime,Uptime last
sign_in,Sign In
last_login,Last Login
admin,Admin
user,User
failed,Failed
wrong_login,Incorrect username or password
theme_editor,Theme Editor
enable_assets,Enable Local Assets
assets_desc,Customize your status page design by enabling local assets. This will create a 'assets' directory containing all CSS.
assets_btn,Enable Local Assets
assets_loading,Creating Assets
assets_dir,Assets Directory
footer,Footer
footer_notes,You can use HTML tags in footer
global_announcement,Global Announcement
announcement_date,Announcement Date Range
notify_users,Notify Users
notify_desc,Notify Users Before Scheduled Time
notify_method,Notification Method
notify_before,Notify Before
message_create,Create Announcement
message_edit,Edit Announcement
minutes,Minutes
hours,Hours
days,Days
user_create,Create User
user_update,Update User
administrator,Administrator
checkins,Checkins
incidents,Incidents
service_info,Service Info
service_name,Service Name
service_type,Service Type
permalink,Permalink URL
service_public,Public Service
check_interval,Check Interval
service_endpoint,Service Endpoint
service_check,Service Check Type
service_timeout,Request Timeout
expected_resp,Expected Response
expected_code,Expected Status Code
follow_redir,Follow Redirects
verify_ssl,Verify SSL
tls_cert,Use TLS Cert
notification_opts,Notification Options
notifications_enable,Enable Notifications
notify_after,Notify After Failures
notify_all,Notify All Changes
service_update,Update Service
service_create,Create Service
1 key en
2 settings Settings
3 dashboard Dashboard
4 services Services
5 service Service
6 failures Failures
7 users Users
8 login Login
9 logout Logout
10 online Online
11 offline Offline
12 configs Configuration
13 username Username
14 password Password
15 email Email
16 confirm_password Confirm Password
17 uptime Uptime
18 name Name
19 copy Copy
20 close Close
21 secret Secret
22 regen_api Regenerate API Keys
23 regen_desc API Secret is used for read create update and delete routes. You can Regenerate API Keys if you need to.
24 visibility Visibility
25 group Group
26 group_create Create Group
27 group_update Update Group
28 group_public_desc Show group services to the public
29 groups Groups
30 no_group No Group
31 public Public
32 private Private
33 announcements Announcements
34 notifiers Notifiers
35 logs Logs
36 help Help
37 type Type
38 edit Edit
39 update Update
40 create Create
41 view View
42 save Save
43 title Title
44 status Status
45 begins Begins
46 total_services Total Services
47 online_services Online Services
48 request_timeout Request Timeout
49 service_never_online Service has never been online
50 service_online_check Online checked
51 service_offline_time Service has been offline for
52 days_ago Days Ago
53 today Today
54 week Week
55 month Month
56 day Day
57 hour Hour
58 minute Minute
59 failures_24_hours Failures last 24 hours
60 no_services You currently don't have any services!
61 theme Theme
62 cache Cache
63 authentication Authentication
64 import Import
65 main_settings Main Settings
66 variables Variables
67 docs Documentation
68 links Links
69 changelog Change Log
70 repo Repository
71 language Language
72 db_connection Database Connection
73 db_host Database Host
74 db_port Database Port
75 db_username Database Username
76 db_password Database Password
77 db_database Database Name
78 send_reports Send Error Reports
79 send_reports_desc Send errors to Statping for debugging
80 project_name Status Page Name
81 description Description
82 domain Domain
83 enable_cdn Enable CDN
84 newsletter Newsletter
85 newsletter_note We will only send you an email for major changes
86 loading Loading
87 save_settings Save Settings
88 average_response Average Response
89 last_uptime Uptime last
90 sign_in Sign In
91 last_login Last Login
92 admin Admin
93 user User
94 failed Failed
95 wrong_login Incorrect username or password
96 theme_editor Theme Editor
97 enable_assets Enable Local Assets
98 assets_desc Customize your status page design by enabling local assets. This will create a 'assets' directory containing all CSS.
99 assets_btn Enable Local Assets
100 assets_loading Creating Assets
101 assets_dir Assets Directory
102 footer Footer
103 footer_notes You can use HTML tags in footer
104 global_announcement Global Announcement
105 announcement_date Announcement Date Range
106 notify_users Notify Users
107 notify_desc Notify Users Before Scheduled Time
108 notify_method Notification Method
109 notify_before Notify Before
110 message_create Create Announcement
111 message_edit Edit Announcement
112 minutes Minutes
113 hours Hours
114 days Days
115 user_create Create User
116 user_update Update User
117 administrator Administrator
118 checkins Checkins
119 incidents Incidents
120 service_info Service Info
121 service_name Service Name
122 service_type Service Type
123 permalink Permalink URL
124 service_public Public Service
125 check_interval Check Interval
126 service_endpoint Service Endpoint
127 service_check Service Check Type
128 service_timeout Request Timeout
129 expected_resp Expected Response
130 expected_code Expected Status Code
131 follow_redir Follow Redirects
132 verify_ssl Verify SSL
133 tls_cert Use TLS Cert
134 notification_opts Notification Options
135 notifications_enable Enable Notifications
136 notify_after Notify After Failures
137 notify_all Notify All Changes
138 service_update Update Service
139 service_create Create Service

View File

@ -1,100 +1,142 @@
const english = {
top_nav: {
settings: "Settings",
dashboard: "Dashboard",
services: "Services",
service: "Service",
failures: "Failures",
users: "Users",
login: "Login",
logout: "Logout",
online: "Online",
offline: "Offline",
configs: "Configuration",
username: "Username",
password: "Password",
email: "Email",
confirm_password: "Confirm Password",
uptime: "Uptime",
name: "Name",
copy: "Copy",
close: "Close",
secret: "Secret",
regen_api: "Regenerate API Keys",
regen_desc: "API Secret is used for read create update and delete routes. You can Regenerate API Keys if you need to.",
visibility: "Visibility",
group: "Group",
group_create: "Create Group",
group_update: "Update Group",
group_public_desc: "Show group services to the public",
groups: "Groups",
no_group: "No Group",
public: "Public",
private: "Private",
announcements: "Announcements",
settings: "Settings",
notifiers: "Notifiers",
logs: "Logs",
logout: 'Logout',
},
setup: {
language: "Language",
connection: "Database Connection",
host: "Host",
database: "Database",
project_name: "Project Name",
project_description: "Project Description",
domain: "Domain URL",
username: "Admin Username",
password: "Admin Password",
password_confirm: "Confirm Admin Password",
newsletter: "Newsletter",
newsletter_note: "We will not share your email, emails are only for major updates.",
send_reports: "Send Error Reports to Statping"
},
dashboard: {
help: "Help",
type: "Type",
edit: "Edit",
update: "Update",
create: "Create",
view: "View",
save: "Save",
title: "Title",
status: "Status",
begins: "Begins",
total_services: "Total Services",
failures_24_hours: "Failures last 24 Hours",
online_services: "Online Services",
service: 'Service | Services',
group: 'Group',
title: 'Title',
begins: 'Begins',
name: 'Name',
loading: 'Loading',
login: 'Login',
sign_in: "Sign In",
visibility: 'Visibility',
wrong_login: 'Incorrect username or password'
},
settings: {
name: "Project Name",
description: "Project Name",
footer: "Custom Footer",
footer_notes: "HTML is allowed inside the footer",
error_reporting: "Enable Error Reporting",
error_reporting_notes: "Help the Statping project out by sending anonymous error logs back to our server.",
save: "Save Settings",
main: "Main Settings",
theme: "Theme Editor",
request_timeout: "Request Timeout",
service_never_online: "Service has never been online",
service_online_check: "Online checked",
service_offline_time: "Service has been offline for",
days_ago: "Days Ago",
today: "Today",
week: "Week",
month: "Month",
day: "Day",
hour: "Hour",
minute: "Minute",
failures_24_hours: "Failures last 24 hours",
no_services: "You currently don't have any services!",
theme: "Theme",
cache: "Cache",
oauth: "OAuth",
beta: "BETA",
changelog: "Changelog",
repo: "Statping Github Repo",
authentication: "Authentication",
import: "Import",
main_settings: "Main Settings",
variables: "Variables",
docs: "Documentation",
},
service: {
name: "Service Name",
type: "Service Type",
info: "Service Information",
view: "View Service",
average: "Average Response",
last_uptime: "Uptime last {0} {1}",
},
email: "Email Address",
port: "Database Port",
setting: "Settings",
username: "Username",
password: 'Password',
services: 'Services',
domain: 'Domain',
online: 'online',
public: 'Public',
private: 'Private',
admin: 'Admin',
offline: 'offline',
failure: 'failure | failures',
incident: 'incident | incidents',
checkin: 'checkin | checkins',
user: 'User | Users',
group: 'Group',
message: 'message',
edit: 'Edit',
type: 'Type',
sample_data: 'Sample Data',
today: 'Today',
last_login: 'Last Login',
uptime: '{0}% Uptime',
close: 'Close',
second: 'second | seconds',
minute: 'minute | minutes',
hour: 'hour | hours',
day: 'day | days',
week: 'week | weeks',
month: 'month | months',
links: "Links",
changelog: "Change Log",
repo: "Repository",
language: "Language",
db_connection: "Database Connection",
db_host: "Database Host",
db_port: "Database Port",
db_username: "Database Username",
db_password: "Database Password",
db_database: "Database Name",
send_reports: "Send Error Reports",
send_reports_desc: "Send errors to Statping for debugging",
project_name: "Status Page Name",
description: "Description",
domain: "Domain",
enable_cdn: "Enable CDN",
newsletter: "Newsletter",
newsletter_note: "We will only send you an email for major changes",
loading: "Loading",
save_settings: "Save Settings",
average_response: "Average Response",
last_uptime: "Uptime last",
sign_in: "Sign In",
last_login: "Last Login",
admin: "Admin",
user: "User",
failed: "Failed",
wrong_login: "Incorrect username or password",
theme_editor: "Theme Editor",
enable_assets: "Enable Local Assets",
assets_desc: "Customize your status page design by enabling local assets. This will create a 'assets' directory containing all CSS.",
assets_btn: "Enable Local Assets",
assets_loading: "Creating Assets",
assets_dir: "Assets Directory",
footer: "Footer",
footer_notes: "You can use HTML tags in footer",
global_announcement: "Global Announcement",
announcement_date: "Announcement Date Range",
notify_users: "Notify Users",
notify_desc: "Notify Users Before Scheduled Time",
notify_method: "Notification Method",
notify_before: "Notify Before",
message_create: "Create Announcement",
message_edit: "Edit Announcement",
minutes: "Minutes",
hours: "Hours",
days: "Days",
user_create: "Create User",
user_update: "Update User",
administrator: "Administrator",
checkins: "Checkins",
incidents: "Incidents",
service_info: "Service Info",
service_name: "Service Name",
service_type: "Service Type",
permalink: "Permalink URL",
service_public: "Public Service",
check_interval: "Check Interval",
service_endpoint: "Service Endpoint",
service_check: "Service Check Type",
service_timeout: "Request Timeout",
expected_resp: "Expected Response",
expected_code: "Expected Status Code",
follow_redir: "Follow Redirects",
verify_ssl: "Verify SSL",
tls_cert: "Use TLS Cert",
notification_opts: "Notification Options",
notifications_enable: "Enable Notifications",
notify_after: "Notify After Failures",
notify_all: "Notify All Changes",
service_update: "Update Service",
service_create: "Create Service"
}
export default english
export default english

View File

@ -1,100 +1,142 @@
const french = {
top_nav: {
dashboard: "Dashboard",
settings: "Paramètres",
dashboard: "Tableau de bord",
services: "Services",
users: "Users",
groups: "Groups",
announcements: "Announcements",
settings: "Settings",
logs: "Logs",
logout: 'Logout',
},
setup: {
language: "Language",
connection: "Database Connection",
host: "Host",
database: "Database",
project_name: "Project Name",
project_description: "Project Description",
domain: "Domain URL",
username: "Admin Username",
password: "Admin Password",
password_confirm: "Confirm Admin Password",
newsletter: "Newsletter",
newsletter_note: "We will not share your email, emails are only for major updates.",
send_reports: "Send Error Reports to Statping"
},
dashboard: {
total_services: "Total Services",
failures_24_hours: "Failures last 24 Hours",
online_services: "Online Services",
service: 'Service | Services',
group: 'Group',
title: 'Title',
begins: 'Begins',
name: 'Name',
loading: 'Loading',
login: 'Login',
sign_in: "Sign In",
visibility: 'Visibility',
wrong_login: 'Incorrect username or password'
},
settings: {
name: "Project Name",
description: "Project Name",
footer: "Custom Footer",
footer_notes: "HTML is allowed inside the footer",
error_reporting: "Enable Error Reporting",
error_reporting_notes: "Help the Statping project out by sending anonymous error logs back to our server.",
save: "Save Settings",
main: "Main Settings",
theme: "Theme Editor",
service: "Service",
failures: "Échecs",
users: "Utilisateurs",
login: "Identifiant",
logout: "Déconnexion",
online: "En ligne",
offline: "Offline",
configs: "Configuration",
username: "Nom d'utilisateur",
password: "mot de passe",
email: "Email",
confirm_password: "Confirmer le",
uptime: "Temps de disponibilité",
name: "Nom",
copy: "Copie",
close: "Fermer",
secret: "Secret",
regen_api: "Régénérer les clés d'API",
regen_desc: "API Secret est utilisé pour lire créer des routes de mise à jour et de suppression. Vous pouvez Régénérer les clés d'API si vous en avez besoin.",
visibility: "Visibilité",
group: "Groupe",
group_create: "Créer un groupe",
group_update: "Groupe Mettre à jour",
group_public_desc: "Afficher les services de groupe au public",
groups: "Groupes",
no_group: "Pas de groupe",
public: "Publique",
private: "Privé",
announcements: "Annonces",
notifiers: "Notifiants",
logs: "Journaux",
help: "Aider",
type: "Type",
edit: "Éditer",
update: "Mise à jour",
create: "Créer",
view: "Voir",
save: "Sauvegarder",
title: "Titre",
status: "Statut",
begins: "Commence",
total_services: "Total des services",
online_services: "Services en ligne",
request_timeout: "Délai d'attente de",
service_never_online: "Le service n'a jamais été en ligne",
service_online_check: "Vérifié en ligne",
service_offline_time: "Le service a été hors ligne pour",
days_ago: "Il y a jours",
today: "Aujourd'hui",
week: "Semaine",
month: "Mois",
day: "Jour",
hour: "Heure",
minute: "Minute",
failures_24_hours: "Les échecs durent 24 heures",
no_services: "Vous n'avez actuellement aucun service !",
theme: "Thème",
cache: "Cache",
oauth: "OAuth",
beta: "BETA",
changelog: "Changelog",
repo: "Statping Github Repo",
authentication: "Authentification",
import: "Importer",
main_settings: "Paramètres principaux",
variables: "Variables",
docs: "Documentation",
},
service: {
name: "Service Name",
type: "Service Type",
info: "Service Information",
view: "View Service",
average: "Average Response",
last_uptime: "Uptime last {0} {1}",
},
email: "Email Address",
port: "Database Port",
setting: "Settings",
username: "Username",
password: 'Password',
services: 'Services',
domain: 'Domain',
online: 'online',
public: 'Public',
private: 'Private',
admin: 'Admin',
offline: 'offline',
failure: 'failure | failures',
incident: 'incident | incidents',
checkin: 'checkin | checkins',
user: 'User | Users',
group: 'Group',
message: 'message',
edit: 'Edit',
type: 'Type',
sample_data: 'Sample Data',
today: 'Today',
last_login: 'Last Login',
uptime: '{0}% Uptime',
close: 'Close',
second: 'second | seconds',
minute: 'minute | minutes',
hour: 'hour | hours',
day: 'day | days',
week: 'week | weeks',
month: 'month | months',
links: "Links",
changelog: "Journal des modifications",
repo: "Référentiel",
language: "Langue",
db_connection: "Connexion à la base",
db_host: "Hôte de base",
db_port: "Port base de données",
db_username: "Nom utilisateur de base",
db_password: "Mot de passe",
db_database: "Nom de la base",
send_reports: "Envoyer des rapports d'erreurs",
send_reports_desc: "Envoyer des erreurs à Statping pour le débogage",
project_name: "Nom de la page d'état",
description: "Description",
domain: "Domaine",
enable_cdn: "Activer le CDN",
newsletter: "Bulletin",
newsletter_note: "Nous ne vous enverrons qu'un e-mail pour les modifications majeures",
loading: "Chargement",
save_settings: "Enregistrer les paramètres",
average_response: "Réponse moyenne",
last_uptime: "Temps de disponibilité dernier",
sign_in: "Se connecter",
last_login: "Dernière connexion",
admin: "Admin",
user: "Utilisateur",
failed: "Échec",
wrong_login: "Nom d'utilisateur ou mot de passe",
theme_editor: "Editeur de thème",
enable_assets: "Activer les actifs locaux",
assets_desc: "Personnalisez la conception de votre page d'état en activant les ressources locales. Cela va créer un répertoire 'assets' contenant tous les CSS.",
assets_btn: "Activer les actifs locaux",
assets_loading: "Création d'actifs",
assets_dir: "Répertoire des actifs",
footer: "Pied de page",
footer_notes: "Vous pouvez utiliser des balises HTML dans le pied de page",
global_announcement: "Annonce mondiale",
announcement_date: "Fourchette de dates d'annonce",
notify_users: "Aviser les utilisateurs",
notify_desc: "Avertir les utilisateurs avant l'heure prévue",
notify_method: "Méthode de notification",
notify_before: "Avertir avant",
message_create: "Créer une annonce",
message_edit: "Modifier l'annonce",
minutes: "Procès-Verbal",
hours: "Heures",
days: "Jours",
user_create: "Créer un utilisateur",
user_update: "Mise à jour utilisateur",
administrator: "Administrateur",
checkins: "Checkins",
incidents: "Incidents",
service_info: "Informations sur le service",
service_name: "Nom du service",
service_type: "Type de service",
permalink: "URL Permalien",
service_public: "Fonction publique",
check_interval: "Période de vérification",
service_endpoint: "Point de terminaison de service",
service_check: "Type de vérification de service",
service_timeout: "Délai d'attente de",
expected_resp: "Réponse attendue",
expected_code: "Code d'état attendu",
follow_redir: "Suivre les redirections",
verify_ssl: "Vérifier SSL",
tls_cert: "Utiliser le certificat TLS",
notification_opts: "Options de notification",
notifications_enable: "Activer les notifications",
notify_after: "Notification après les échecs",
notify_all: "Notifier toutes les modifications",
service_update: "Mise à jour Service",
service_create: "Créer un service"
}
export default french
export default french

View File

@ -1,100 +1,142 @@
const german = {
top_nav: {
dashboard: "Dashboard",
services: "Services",
users: "Users",
groups: "Groups",
announcements: "Announcements",
settings: "Settings",
logs: "Logs",
logout: 'Logout',
},
setup: {
language: "Language",
connection: "Database Connection",
host: "Host",
database: "Database",
project_name: "Project Name",
project_description: "Project Description",
domain: "Domain URL",
username: "Admin Username",
password: "Admin Password",
password_confirm: "Confirm Admin Password",
newsletter: "Newsletter",
newsletter_note: "We will not share your email, emails are only for major updates.",
send_reports: "Send Error Reports to Statping"
},
dashboard: {
total_services: "Total Services",
failures_24_hours: "Failures last 24 Hours",
online_services: "Online Services",
service: 'Service | Services',
group: 'Group',
title: 'Title',
begins: 'Begins',
name: 'Name',
loading: 'Loading',
login: 'Login',
sign_in: "Sign In",
visibility: 'Visibility',
wrong_login: 'Incorrect username or password'
},
settings: {
name: "Project Name",
description: "Project Name",
footer: "Custom Footer",
footer_notes: "HTML is allowed inside the footer",
error_reporting: "Enable Error Reporting",
error_reporting_notes: "Help the Statping project out by sending anonymous error logs back to our server.",
save: "Save Settings",
main: "Main Settings",
theme: "Theme Editor",
settings: "Einstellungen",
dashboard: "Armaturenbrett",
services: "Dienstleistungen",
service: "Service",
failures: "Ausfälle",
users: "Benutzer",
login: "Login",
logout: "Abmelden",
online: "Online",
offline: "Offline",
configs: "Konfiguration",
username: "Benutzername",
password: "Kennwort",
email: "Mail",
confirm_password: "Passwort bestätigen",
uptime: "Betriebszeit",
name: "Name",
copy: "Kopie",
close: "Schließen",
secret: "Geheimnis",
regen_api: "API-Schlüssel neu generieren",
regen_desc: "API Secret wird für Lesen erstellen Update und Löschen Routen verwendet. Sie können API-Schlüssel bei Bedarf neu generieren.",
visibility: "Sichtbarkeit",
group: "Gruppe",
group_create: "Gruppe erstellen",
group_update: "Gruppe aktualisieren",
group_public_desc: "Gruppendienste für die Öffentlichkeit anzeigen",
groups: "Gruppen",
no_group: "Keine Gruppe",
public: "Öffentlichkeit",
private: "Privat",
announcements: "Ankündigungen",
notifiers: "Notifizierer",
logs: "Protokolle",
help: "Helfen",
type: "Typ",
edit: "Bearbeiten",
update: "Update",
create: "Schaffen",
view: "Ansicht",
save: "sparen",
title: "Titel",
status: "Status",
begins: "Beginnt",
total_services: "Dienstleistungen insgesamt",
online_services: "Online-Dienste",
request_timeout: "Zeitüberschreitung anfordern",
service_never_online: "Service war noch nie online",
service_online_check: "Online geprüft",
service_offline_time: "Dienst war offline für",
days_ago: "Vor Tagen",
today: "Heute",
week: "Woche",
month: "Monat",
day: "Tag",
hour: "Stunde",
minute: "Minute",
failures_24_hours: "Ausfälle dauern 24 Stunden",
no_services: "Sie haben derzeit keine Dienste!",
theme: "Thema",
cache: "Cache",
oauth: "OAuth",
beta: "BETA",
changelog: "Changelog",
repo: "Statping Github Repo",
docs: "Documentation",
},
service: {
name: "Service Name",
type: "Service Type",
info: "Service Information",
view: "View Service",
average: "Average Response",
last_uptime: "Uptime last {0} {1}",
},
email: "Email Address",
port: "Database Port",
setting: "Settings",
username: "Username",
password: 'Password',
services: 'Services',
domain: 'Domain',
online: 'online',
public: 'Public',
private: 'Private',
admin: 'Admin',
offline: 'offline',
failure: 'failure | failures',
incident: 'incident | incidents',
checkin: 'checkin | checkins',
user: 'User | Users',
group: 'Group',
message: 'message',
edit: 'Edit',
type: 'Type',
sample_data: 'Sample Data',
today: 'Today',
last_login: 'Last Login',
uptime: '{0}% Uptime',
close: 'Close',
second: 'second | seconds',
minute: 'minute | minutes',
hour: 'hour | hours',
day: 'day | days',
week: 'week | weeks',
month: 'month | months',
authentication: "Authentifikation",
import: "Importieren",
main_settings: "Haupteinstellungen",
variables: "Variablen",
docs: "Dokumentation",
links: "Links",
changelog: "Protokoll ändern",
repo: "Depot",
language: "Sprache",
db_connection: "Datenbankverbindung",
db_host: "Datenbank-Host",
db_port: "Datenbank-Port",
db_username: "Datenbank-Benutzername",
db_password: "Datenbank-Kennwort",
db_database: "Name der Datenbank",
send_reports: "Fehlerberichte senden",
send_reports_desc: "Fehler zum Debuggen an Statping senden",
project_name: "Name der Seite „Status“",
description: "Beschreibung",
domain: "Domäne",
enable_cdn: "CDN aktivieren",
newsletter: "Newsletter",
newsletter_note: "Wir senden Ihnen nur eine E-Mail für größere Änderungen",
loading: "Laden",
save_settings: "Einstellungen speichern",
average_response: "Durchschnittliche Antwort",
last_uptime: "Betriebszeit zuletzt",
sign_in: "Anmelden",
last_login: "Letzte Anmeldung",
admin: "Admin",
user: "Benutzer",
failed: "Fehlgeschlagen",
wrong_login: "Falscher Benutzername oder Passwort",
theme_editor: "Theme-Editor",
enable_assets: "Lokale Assets aktivieren",
assets_desc: "Passen Sie das Design Ihrer Statusseite an, indem Sie lokale Elemente aktivieren. Dadurch wird ein Verzeichnis „Assets“ erstellt, das alle CSS enthält.",
assets_btn: "Lokale Assets aktivieren",
assets_loading: "Erstellen von Assets",
assets_dir: "Assets Verzeichnis",
footer: "Fußzeile",
footer_notes: "Sie können HTML-Tags in der Fußzeile verwenden",
global_announcement: "Globale Ankündigung",
announcement_date: "Datumsbereich für Ankündigung",
notify_users: "Benutzer benachrichtigen",
notify_desc: "Benutzer vor geplanter Zeit benachrichtigen",
notify_method: "Benachrichtigungsmethode",
notify_before: "Vor benachrichtigen",
message_create: "Ankündigung erstellen",
message_edit: "Ankündigung bearbeiten",
minutes: "Protokoll",
hours: "Stunden",
days: "Tage",
user_create: "Benutzer erstellen",
user_update: "Benutzer aktualisieren",
administrator: "Administrator",
checkins: "Checkins",
incidents: "Vorfälle",
service_info: "Service-Info",
service_name: "Dienstname",
service_type: "Service-Art",
permalink: "Permalink-URL",
service_public: "Öffentlicher Dienst",
check_interval: "Intervall prüfen",
service_endpoint: "Service-Endpunkt",
service_check: "Service-Prüfungstyp",
service_timeout: "Zeitüberschreitung anfordern",
expected_resp: "Erwartete Antwort",
expected_code: "Erwarteter Statuscode",
follow_redir: "Weiterleitungen folgen",
verify_ssl: "SSL verifizieren",
tls_cert: "TLS-Zertifikat verwenden",
notification_opts: "Benachrichtigungsoptionen",
notifications_enable: "Benachrichtigungen aktivieren",
notify_after: "Benachrichtigung nach Fehlern",
notify_all: "Alle Änderungen benachrichtigen",
service_update: "Dienst aktualisieren",
service_create: "Dienst erstellen"
}
export default german
export default german

View File

@ -3,6 +3,10 @@ import spanish from "./spanish"
import german from "./german"
import russian from "./russian";
import french from "./french";
import japanese from "./japanese";
import chinese from "./chinese";
import korean from "./korean";
import italian from "./italian";
const language = {
en: english,
@ -10,6 +14,10 @@ const language = {
de: german,
ru: russian,
fr: french,
ja: japanese,
zh: chinese,
ko: korean,
it: italian,
}
export default language

142
frontend/src/languages/italian.js Executable file
View File

@ -0,0 +1,142 @@
const italian = {
settings: "Impostazioni",
dashboard: "cruscotto",
services: "Servizi",
service: "Servizio",
failures: "Fallimenti",
users: "Utenti",
login: "Login",
logout: "Disconnetti",
online: "Online",
offline: "Offline",
configs: "Configurazione",
username: "Nome utente",
password: "Password",
email: "E-mail",
confirm_password: "Conferma password",
uptime: "Tempi di attività",
name: "Nome",
copy: "Copia",
close: "Vicino",
secret: "Segreto",
regen_api: "Rigenera chiavi API",
regen_desc: "API Secret viene utilizzato per leggere creare aggiornamento ed eliminare route. Se necessario, è possibile rigenerare le chiavi API.",
visibility: "Visibilità",
group: "Gruppo",
group_create: "Crea gruppo",
group_update: "Aggiorna gruppo",
group_public_desc: "Mostra i servizi di gruppo al pubblico",
groups: "Gruppi",
no_group: "Nessun gruppo",
public: "Pubblico",
private: "Privato",
announcements: "Comunicazioni",
notifiers: "Notificatori",
logs: "Log",
help: "Guida",
type: "Tipo",
edit: "Modifica",
update: "Aggiorna",
create: "Crea",
view: "Visualizza",
save: "Salvare",
title: "Titolo",
status: "Stato",
begins: "Inizia",
total_services: "Totale servizi",
online_services: "Servizi online",
request_timeout: "Timeout richiesta",
service_never_online: "Il servizio non è mai stato online",
service_online_check: "Controllato online",
service_offline_time: "Il servizio è stato offline per",
days_ago: "Giorni fa",
today: "Oggi",
week: "Settimana",
month: "Mese",
day: "Giorno",
hour: "Ora",
minute: "Minuti",
failures_24_hours: "Errori nelle ultime 24 ore",
no_services: "Al momento non hai alcun servizio!",
theme: "Tema",
cache: "Cache",
authentication: "autenticazione",
import: "Importa",
main_settings: "Impostazioni principali",
variables: "Variabili",
docs: "Documentazione",
links: "Collegamenti",
changelog: "Registro delle modifiche",
repo: "repository",
language: "Lingua",
db_connection: "Connessione al database",
db_host: "Host del database",
db_port: "Porta del database",
db_username: "Nome utente del database",
db_password: "Password del database",
db_database: "Nome database",
send_reports: "Invia segnalazioni errori",
send_reports_desc: "Invia errori a Statping per il debug",
project_name: "Nome pagina di stato",
description: "Descrizione",
domain: "Dominio",
enable_cdn: "Abilita CDN",
newsletter: "Bollettino",
newsletter_note: "Ti invieremo solo una e-mail per le modifiche più importanti",
loading: "Caricamento",
save_settings: "Salva impostazioni",
average_response: "Risposta media",
last_uptime: "Ultimo tempo di attività",
sign_in: "Accedi",
last_login: "Ultimo Login",
admin: "Amministratore",
user: "Utente",
failed: "Non riuscito",
wrong_login: "Nome utente o password non corretti",
theme_editor: "Editor tema",
enable_assets: "Abilita risorse locali",
assets_desc: "Personalizza la progettazione della pagina di stato abilitando le risorse locali. Questo creerà una directory 'asset' contenente tutti i CSS.",
assets_btn: "Abilita risorse locali",
assets_loading: "Creazione di risorse",
assets_dir: "Directory delle risorse",
footer: "Piè di pagina",
footer_notes: "È possibile utilizzare i tag HTML nel piè di pagina",
global_announcement: "Annuncio globale",
announcement_date: "Intervallo di date di annuncio",
notify_users: "Notify agli utenti",
notify_desc: "Notifichi agli utenti prima dell'ora pianific",
notify_method: "Metodo di notifica",
notify_before: "Notifichi prima",
message_create: "Crea annuncio",
message_edit: "Modifica annuncio",
minutes: "Minuti",
hours: "Ore",
days: "Giorni",
user_create: "Crea utente",
user_update: "Aggiorna utente",
administrator: "Amministratore",
checkins: "Check-ins",
incidents: "Incidenti",
service_info: "Info servizio",
service_name: "Nome servizio",
service_type: "Tipo di servizio",
permalink: "URL Permalink",
service_public: "Servizio pubblico",
check_interval: "Intervallo controllo",
service_endpoint: "Endpoint servizio",
service_check: "Tipo di controllo del servizio",
service_timeout: "Timeout richiesta",
expected_resp: "Risposta prevista",
expected_code: "Codice di stato previsto",
follow_redir: "Segui i reindirizzamenti",
verify_ssl: "Verifica SSL",
tls_cert: "Usa certificato TLS",
notification_opts: "Opzioni di notifica",
notifications_enable: "Abilita notifiche",
notify_after: "Notify dopo gli errori",
notify_all: "Notifichi tutte le modifiche",
service_update: "Servizio di aggiornamento",
service_create: "Crea servizio"
}
export default italian

View File

@ -0,0 +1,142 @@
const japanese = {
settings: "設定",
dashboard: "ダッシュボード",
services: "サービス",
service: "サービス",
failures: "障害",
users: "ユーザー",
login: "ログイン",
logout: "ログアウト",
online: "オンライン",
offline: "オフライン",
configs: "構成",
username: "ユーザ名",
password: "パスワード",
email: "Eメール",
confirm_password: "パスワードの確認",
uptime: "稼働時間",
name: "氏名",
copy: "コピー",
close: "閉じる",
secret: "秘密",
regen_api: "API キーの再生成",
regen_desc: "APIシークレットは、ルート作成の更新と削除の読み込みに使用されます。必要に応じて、API キーを再生成できます。",
visibility: "可視性",
group: "グループ",
group_create: "[グループを作成]",
group_update: "[グループを更新]",
group_public_desc: "グループサービスを一般公開する",
groups: "グループ",
no_group: "グループなし",
public: "パブリック",
private: "私立",
announcements: "アナウンス",
notifiers: "通知者",
logs: "ログ",
help: "ヘルプ",
type: "タイプ",
edit: "編集",
update: "更新",
create: "作成",
view: "ビュー",
save: "保存する",
title: "タイトル",
status: "ステータス",
begins: "開始する",
total_services: "トータルサービス",
online_services: "オンラインサービス",
request_timeout: "リクエストのタイムアウト",
service_never_online: "サービスがオンラインになったことがない",
service_online_check: "オンラインチェック済み",
service_offline_time: "のサービスがオフラインになりました",
days_ago: "日前",
today: "今日",
week: "週数",
month: "月",
day: "日",
hour: "アワー",
minute: "分",
failures_24_hours: "過去 24 時間の障害",
no_services: "現在、サービスをお持ちになりません。",
theme: "テーマ",
cache: "キャッシュ",
authentication: "認証",
import: "インポート",
main_settings: "メイン設定",
variables: "変数",
docs: "ドキュメント",
links: "リンク",
changelog: "変更ログ",
repo: "リポジトリ",
language: "言語",
db_connection: "データベース接続",
db_host: "データベース・ホスト",
db_port: "データベースポート",
db_username: "データベースのユーザー名",
db_password: "データベースパスワード",
db_database: "データベース名",
send_reports: "エラーレポートを送信",
send_reports_desc: "デバッグのためにエラーを Statping に送信する",
project_name: "ステータスページ名",
description: "説明",
domain: "ドメイン",
enable_cdn: "CDN を有効にする",
newsletter: "ニュースレター",
newsletter_note: "大きな変更についてのみメールをお送りします",
loading: "ロード中",
save_settings: "設定を保存",
average_response: "平均応答",
last_uptime: "稼働時間最後",
sign_in: "サインイン",
last_login: "最終ログイン",
admin: "管理者",
user: "ユーザー",
failed: "失敗しました",
wrong_login: "ユーザー名またはパスワードが正しくありません",
theme_editor: "テーマ・エディター",
enable_assets: "ローカルアセットを有効にする",
assets_desc: "ローカルアセットを有効にして、ステータスページのデザインをカスタマイズします。これにより、すべてのCSSを含む「assets」ディレクトリが作成されます。",
assets_btn: "ローカルアセットを有効にする",
assets_loading: "アセットの作成",
assets_dir: "アセットディレクトリ",
footer: "フッター",
footer_notes: "あなたはフッターにHTMLタグを使用することができます",
global_announcement: "グローバル発表",
announcement_date: "発表日の範囲",
notify_users: "ユーザーに通知",
notify_desc: "スケジュールされた時間前にユーザーに通知する",
notify_method: "通知方法",
notify_before: "前に通知する",
message_create: "アナウンスの作成",
message_edit: "アナウンスの編集",
minutes: "分単位",
hours: "時間",
days: "日数",
user_create: "ユーザーの作成",
user_update: "ユーザーの更新",
administrator: "管理者",
checkins: "チェックイン",
incidents: "事変",
service_info: "サービス情報",
service_name: "サービス名",
service_type: "サービスタイプ",
permalink: "パーマリンクURL",
service_public: "公共サービス",
check_interval: "チェック間隔",
service_endpoint: "サービスエンドポイント",
service_check: "サービス・チェック・タイプ",
service_timeout: "リクエストのタイムアウト",
expected_resp: "期待される応答",
expected_code: "予想されるステータスコード",
follow_redir: "リダイレクトに従う",
verify_ssl: "SSL の確認",
tls_cert: "TLS 証明書を使用",
notification_opts: "通知オプション",
notifications_enable: "通知を有効にする",
notify_after: "障害発生後に通知する",
notify_all: "すべての変更を通知",
service_update: "サービスの更新",
service_create: "サービスの作成"
}
export default japanese

Some files were not shown because too many files have changed in this diff Show More