diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 3a4091c0..ea9feaad 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -42,7 +42,9 @@ jobs: env: VERSION: ${{ env.VERSION }} COMMIT: ${{ github.sha }} - run: make clean compile + MJML_APP: ${{ secrets.MJML_APP }} + MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }} + run: make clean generate compile - name: Upload Compiled Frontend (rice-box.go) uses: actions/upload-artifact@v1 @@ -98,8 +100,22 @@ jobs: shell: bash - name: Set Linux Build Flags - if: matrix.platform != 'darwin' - run: echo ::set-env name=BUILD_FLAGS::'-extldflags -static' + 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 }} @@ -117,6 +133,7 @@ jobs: 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 @@ -262,6 +279,10 @@ jobs: 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: | diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 6cabf8c8..5ec814fd 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -42,7 +42,9 @@ jobs: env: VERSION: ${{ env.VERSION }} COMMIT: ${{ github.sha }} - run: make clean compile + MJML_APP: ${{ secrets.MJML_APP }} + MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }} + run: make clean generate compile - name: Upload Compiled Frontend (rice-box.go) uses: actions/upload-artifact@v1 @@ -98,8 +100,22 @@ jobs: shell: bash - name: Set Linux Build Flags - if: matrix.platform != 'darwin' - run: echo ::set-env name=BUILD_FLAGS::'-extldflags -static' + 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 }} @@ -117,6 +133,7 @@ jobs: 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 @@ -262,6 +279,10 @@ jobs: 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: | diff --git a/.gitignore b/.gitignore index c15bfc58..d0b07313 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ tmp /frontend/cypress/screenshots/ /frontend/cypress/videos/ services.yml +statping.wiki diff --git a/CHANGELOG.md b/CHANGELOG.md index 68624b4b..588e4c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# 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 diff --git a/Makefile b/Makefile index 94693cb4..571873b9 100644 --- a/Makefile +++ b/Makefile @@ -21,14 +21,14 @@ test: clean compile go test -v -p=1 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -coverprofile=coverage.out ./... build: clean - go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo linux" ./cmd + 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" ./cmd + 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 ./... @@ -76,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 ./... @@ -420,5 +421,13 @@ check: # 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 diff --git a/cmd/main.go b/cmd/main.go index e9f0072c..85bec417 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -29,10 +29,10 @@ var ( func init() { stopped = make(chan bool, 1) - core.New(VERSION) + core.New(VERSION, COMMIT) utils.InitEnvs() - configs.Version = VERSION - configs.Commit = COMMIT + utils.Params.Set("VERSION", VERSION) + utils.Params.Set("COMMIT", COMMIT) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(updateCmd) @@ -161,7 +161,7 @@ func InitApp() error { // start routine to delete old records (failures, hits) go database.Maintenance() // init Sentry error monitoring (its useful) - utils.SentryInit(&VERSION, core.App.AllowReports.Bool) + utils.SentryInit(core.App.AllowReports.Bool) core.App.Setup = true core.App.Started = utils.Now() return nil diff --git a/database/grouping.go b/database/grouping.go index 6d982a3b..ff953e24 100644 --- a/database/grouping.go +++ b/database/grouping.go @@ -71,13 +71,11 @@ 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 = g.db.MultipleSelects( g.db.SelectByTime(g.Group), by.String(), ).Group("timeframe").Order("timeframe", true) - g.db = dbQuery - caller, err := g.ToTimeValue() if err != nil { return nil, err @@ -89,6 +87,9 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) { return caller.ToValues() } +// 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 (g *GroupQuery) ToTimeValue() (*TimeVar, error) { rows, err := g.db.Rows() if err != nil { @@ -116,27 +117,26 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) { 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) } diff --git a/database/time.go b/database/time.go index ea41f77d..db690bab 100644 --- a/database/time.go +++ b/database/time.go @@ -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 -} diff --git a/dev/postman.json b/dev/postman.json index 17798522..f21fd2b1 100644 --- a/dev/postman.json +++ b/dev/postman.json @@ -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\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\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.", @@ -4099,7 +4198,7 @@ "", "pm.test(\"View All Notifiers\", function () {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.length).to.eql(12);", + " pm.expect(jsonData.length).to.eql(13);", "});" ], "type": "text/javascript" @@ -4303,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": {} } @@ -4415,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": {} } diff --git a/frontend/package.json b/frontend/package.json index 731eb014..46b06aac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "@fortawesome/vue-fontawesome": "^0.1.9", "@sentry/browser": "^5.20.1", "@sentry/integrations": "^5.20.1", - "apexcharts": "^3.15.0", + "apexcharts": "^3.6.6", "axios": "^0.19.1", "codemirror-colorpicker": "^1.9.66", "core-js": "^3.6.5", @@ -29,8 +29,9 @@ "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", @@ -75,6 +76,7 @@ "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", diff --git a/frontend/src/API.js b/frontend/src/API.js index 5aee79b6..49ef994c 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -7,7 +7,8 @@ const tokenKey = "statping_auth"; class Api { constructor() { - + this.version = "0.90.64"; + this.commit = "130cc3ede7463ec9af8d62abb84992e2a0ef453c"; } async oauth() { @@ -51,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)) @@ -72,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) { @@ -87,16 +88,16 @@ 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) + window.console.log('api/reorder/groups', data) return axios.post('api/reorder/groups', data).then(response => (response.data)) } @@ -129,40 +130,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(api) { - return axios.get('api/checkins/'+api).then(response => (response.data)) + return axios.get('api/checkins/' + api).then(response => (response.data)) } - async checkin_create(data) { - return axios.post('api/checkins', data).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 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)) @@ -270,6 +271,10 @@ class Api { } } + async github_release() { + return fetch('https://api.github.com/repos/statping/statping/releases/latest').then(response => response.json()) + } + async allActions(...all) { await axios.all([all]) } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 999cd1b4..afa27a8c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/assets/scss/base.scss b/frontend/src/assets/scss/base.scss index e5bc7242..461b72f6 100644 --- a/frontend/src/assets/scss/base.scss +++ b/frontend/src/assets/scss/base.scss @@ -83,13 +83,13 @@ } .chartmarker { - padding: 5px; - width: 240px; + padding: 0px; + width: 200px; text-align: right; } .chartmarker SPAN { - font-size: 9pt; + font-size: 4pt; display: block; color: #8b8b8b; } @@ -103,11 +103,33 @@ 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 { @@ -154,10 +176,6 @@ padding: 5px 7px; } - .service_li { - min-height: 115px !important; - } - .btn-sm { line-height: 1.3; font-size: 0.75rem; @@ -289,10 +307,6 @@ color: #a0a0a0; } - .service_block { - min-height: 340px; - } - .json-field { font-size: 10pt; } diff --git a/frontend/src/assets/scss/layout.scss b/frontend/src/assets/scss/layout.scss index 231f4151..2bd6e5ec 100644 --- a/frontend/src/assets/scss/layout.scss +++ b/frontend/src/assets/scss/layout.scss @@ -92,19 +92,45 @@ A:HOVER { 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, 2%) !important; + 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; @@ -113,16 +139,40 @@ A:HOVER { 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 A { +.footer .links { color: $footer-text-color; text-decoration: none; } -.footer A:HOVER { - color: #6d6d6d; +.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; } diff --git a/frontend/src/assets/scss/mobile.scss b/frontend/src/assets/scss/mobile.scss index 2b9e0338..c0fb79df 100644 --- a/frontend/src/assets/scss/mobile.scss +++ b/frontend/src/assets/scss/mobile.scss @@ -40,10 +40,6 @@ font-size: 0.9rem; } - .service_li { - border: 1px solid #f3f3f3 !important; - } - .container { padding: 0px !important; padding-top: 0vh !important; diff --git a/frontend/src/assets/scss/variables.scss b/frontend/src/assets/scss/variables.scss index bec1b6a9..959218cd 100644 --- a/frontend/src/assets/scss/variables.scss +++ b/frontend/src/assets/scss/variables.scss @@ -2,20 +2,20 @@ $background-color: #EAEAEA; $container-color: #ffffff; $text-color: #2a2a2a; -$max-width: 860px; +$max-width: 1012px; $title-color: #4e4e4e; $description-color: #828282; $subtitle-color: #747474; $mobile-card-shadow: 2px 3px 10px #b7b7b7; -$group-list-background: #fafafa; +$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: #20ac13; +$day-success-background: #e9e9e9; $day-error-background: #d50a0a; /* Status Container */ @@ -35,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; diff --git a/frontend/src/components/Dashboard/DashboardIndex.vue b/frontend/src/components/Dashboard/DashboardIndex.vue index 11a98eba..7f161ff2 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.vue +++ b/frontend/src/components/Dashboard/DashboardIndex.vue @@ -1,6 +1,5 @@ diff --git a/frontend/src/components/Dashboard/ServiceEvents.vue b/frontend/src/components/Dashboard/ServiceEvents.vue index 7a5340f1..f325c4b9 100644 --- a/frontend/src/components/Dashboard/ServiceEvents.vue +++ b/frontend/src/components/Dashboard/ServiceEvents.vue @@ -1,28 +1,33 @@ @@ -34,7 +39,6 @@ export default { name: "ServiceEvents", components: { Loading - }, props: { service: { @@ -45,7 +49,6 @@ name: "ServiceEvents", data() { return { incidents: null, - failure: null, loaded: false, } }, @@ -53,16 +56,28 @@ name: "ServiceEvents", 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 - if (!this.service.online) { - await this.getFailure() - } await this.getMessages() await this.getIncidents() this.loaded = true @@ -70,10 +85,6 @@ name: "ServiceEvents", async getMessages() { // this.messages = this.$store.getters.serviceMessages(this.service.id) }, - async getFailure() { - const f = await Api.service_failures(this.service.id, null, null, 1) - this.failure = f[0] - }, async getIncidents() { this.incidents = await Api.incidents_service(this.service.id) }, diff --git a/frontend/src/components/Dashboard/ServiceInfo.vue b/frontend/src/components/Dashboard/ServiceInfo.vue index acd62c6a..57da18c2 100644 --- a/frontend/src/components/Dashboard/ServiceInfo.vue +++ b/frontend/src/components/Dashboard/ServiceInfo.vue @@ -1,64 +1,56 @@