Revert "Revert "speed up ci builds from 15 to <7 minutes"" (#1643)

Reverts grafana/oncall#1639
This commit is contained in:
Joey Orlando 2023-03-28 09:34:03 +02:00 committed by GitHub
parent 61608beee6
commit 0eb4bd95e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 562 additions and 624 deletions

View file

@ -1,211 +0,0 @@
name: ci
on:
push:
branches:
- main
- dev
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
jobs:
lint:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
- name: Build
run: |
pip install $(grep "pre-commit" engine/requirements.txt)
npm install -g yarn
cd grafana-plugin/
yarn --network-timeout 500000
yarn build
# pre-commit uses git, which is not working in the action without this workaround
# see https://github.com/actions/runner-images/issues/6775
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Lint All
run: |
pre-commit run --all-files
test-frontend:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
- name: Unit Testing Frontend
run: |
pip install $(grep "pre-commit" engine/requirements.txt)
npm install -g yarn
cd grafana-plugin/
yarn --network-timeout 500000
yarn test
test-technical-documentation:
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: "actions/checkout@v3"
- name: "Build website"
# -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside
# that no refs to external content can be used as these refs will not resolve in the
# docs-base image.
run: |
docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
lint-migrations-backend-mysql-rabbitmq:
name: "Lint migrations"
runs-on: ubuntu-latest
container: python:3.9
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
steps:
- uses: actions/checkout@v3
- name: Lint migrations
run: |
cd engine/
pip install -r requirements.txt
python manage.py lintmigrations
unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
./wait_for_test_mysql_start.sh && ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: postgresql
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
postgresql_test:
image: postgres:14.4
env:
POSTGRES_DB: oncall_local_dev
POSTGRES_PASSWORD: local_dev_pwd
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
cd engine/
pip install -r requirements.txt
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: sqlite3
BROKER_TYPE: redis
REDIS_URI: redis://redis_test:6379
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
redis_test:
image: redis:7.0.5
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-pd-migrator:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v2
- name: Unit Test PD Migrator
run: |
cd tools/pagerduty-migrator
pip install -r requirements.txt
pytest -x
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Test docker build (no push)
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
target: prod
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,4 +1,4 @@
name: helm-release
name: Helm Release
on:
push:

View file

@ -1,294 +0,0 @@
name: Integration Tests
on:
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
# TODO: ideally we would be able to have one CI job which spins up the kind cluster and does the helm release
# then we could have the UI and backend integration tests dependent on this job and not have to each
# independently spin up the cluster. This doesn't seem to be supported however
# https://github.com/docker/build-push-action/issues/225
#
# Probably one way to get around this would be to deploy the helm release to a sandbox k8s cluster somewhere? and reference
# that in the various integration test jobs
jobs:
build-engine-docker-image:
name: Build engine Docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx # We need this step for docker caching
uses: docker/setup-buildx-action@v2
- name: Build docker image locally # using github actions docker cache
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
load: true
tags: oncall/engine:latest
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/oncall-engine.tar
# https://github.com/docker/build-push-action/issues/225#issuecomment-727639184
- name: Persist engine Docker image between jobs
uses: actions/upload-artifact@v2
with:
name: oncall-engine
path: /tmp/oncall-engine.tar
backend-integration-tests:
name: Backend Integration Tests
needs: build-engine-docker-image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download engine Docker image
uses: actions/download-artifact@v2
with:
name: oncall-engine
path: /tmp
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- name: Load image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
- name: Install helm chart
run: |
helm install test-release \
--values ./simple.yml \
--values ./values-local-image.yml \
./oncall
working-directory: helm
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: always()
- name: Bootstrap organization and integration
run: |
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}")
export ONCALL_INTEGRATION_URL=http://localhost:30001$(kubectl exec -it $POD_NAME -- bash -c "python manage.py setup_end_to_end_test --bootstrap_integration")
echo "ONCALL_INTEGRATION_URL=$ONCALL_INTEGRATION_URL" >> $GITHUB_ENV
- name: Send an alert to the integration
run: |
echo $ONCALL_INTEGRATION_URL
export TEST_ID=test-0
echo "TEST_ID=$TEST_ID" >> $GITHUB_ENV
curl -X POST "$ONCALL_INTEGRATION_URL" \
-H 'Content-Type: Application/json' \
-d '{
"alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552",
"title": "'"$TEST_ID"'",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"state": "alerting",
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
"message": "Smth happened. Oh no!"
}'
- name: Await 1 alert group and 1 alert created during the test (timeout 30 seconds)
run: |
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}")
tries=30
while [ "$tries" -gt 0 ]; do
if kubectl exec -it $POD_NAME -c oncall -- bash -c "python manage.py setup_end_to_end_test --return_results_for_test_id $TEST_ID" | grep -q '1, 1'
then
break
fi
tries=$(( tries - 1 ))
sleep 1
done
if [ "$tries" -eq 0 ]; then
echo 'Expected "1, 1" (alert groups, alerts). They were not created in 30 seconds during this integration test. Something is broken' >&2
exit 1
fi
ui-integration-tests:
needs: build-engine-docker-image
runs-on: ubuntu-latest
name: "UI Integration Tests - Grafana: ${{ matrix.grafana-image-tag }}"
strategy:
matrix:
grafana-image-tag:
- 9.2.6
- main
- latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download engine Docker image
uses: actions/download-artifact@v2
with:
name: oncall-engine
path: /tmp
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- name: Load image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
# yarn caching doesn't properly work with subdirectories hence the following two steps
# which calculate a cache key and restore the cache manually
# see this GH issue for more details https://github.com/actions/setup-node/issues/488#issue-1231822552
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
shell: bash
working-directory: ./grafana-plugin
- name: Restore yarn cache
uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
restore-keys: |
yarn-cache-folder-
# https://stackoverflow.com/a/62244232
# --prefer-offline tells yarn to use cached downloads (in the cache directory mentioned above)
# during installation whenever possible instead of downloading from the server.
- name: Install dependencies
working-directory: ./grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline
# build the plugin frontend
- name: Build plugin frontend
working-directory: ./grafana-plugin
run: yarn build:dev
# by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built
# OnCall plugin rather than the latest published version
# the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir
# see ./helm/kind.yml for more details
- name: Install helm chart
run: |
helm install helm-testing \
--values ./helm/simple.yml \
--values ./helm/values-local-image.yml \
--set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \
--set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \
--set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \
--set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \
--set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \
--set grafana.image.tag=${{ matrix.grafana-image-tag }} \
--set grafana.env.GF_SECURITY_ADMIN_USER=oncall \
--set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \
--set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \
--set-json "grafana.plugins=[]" \
--set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \
--set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \
./helm/oncall
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1154603556
# Figures out the version of playwright that's installed.
# The result is stored in steps.playwright-version.outputs.version
- name: Get installed Playwright version
id: playwright-version
working-directory: ./grafana-plugin
run: echo "::set-output name=version::$(yarn list --pattern @playwright/test | grep @playwright/test@ | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')"
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1317670494
# Attempt to restore the correct Playwright browser binaries based on the
# currently installed version of Playwright (The browser binary versions
# may change with Playwright versions).
# Note: Playwright's cache directory is hard coded because that's what it
# says to do in the docs. There doesn't appear to be a command that prints
# it out for us.
- name: Cache Playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: "~/.cache/ms-playwright"
key: "${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}"
# TODO: If the Playwright browser binaries weren't able to be restored, install them
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1256878540
- name: Install Playwright
# if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: ./grafana-plugin
run: |
npx playwright install
npx playwright install-deps
# - name: Install Playwright system dependencies
# run: npx playwright install-deps
# if: steps.playwright-cache.outputs.cache-hit == 'true'
# working-directory: ./grafana-plugin
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: always()
- name: Run Integration Tests
env:
# BASE_URL represents what is accessed via a browser
BASE_URL: http://localhost:30002/grafana
# ONCALL_API_URL is what is configured in the plugin configuration form
# it is what the grafana container uses to communicate with the OnCall backend
#
# 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container
# hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped
# to the OnCall API
ONCALL_API_URL: http://172.17.0.1:30001
GRAFANA_USERNAME: oncall
GRAFANA_PASSWORD: oncall
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
working-directory: ./grafana-plugin
run: yarn test:integration
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: ./grafana-plugin/playwright-report/
retention-days: 30

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions

427
.github/workflows/linting-and-tests.yml vendored Normal file
View file

@ -0,0 +1,427 @@
name: Linting and Unit/e2e Tests
on:
push:
branches:
- main
- dev
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
concurrency:
# Cancel any running workflow for the same branch when new commits are pushed.
# We group both by ref_name (available when CI is triggered by a push to a branch/tag)
# and head_ref (available when CI is triggered by a PR).
group: "${{ github.ref_name }}-${{ github.head_ref }}"
cancel-in-progress: true
jobs:
lint-entire-project:
name: "Lint entire project"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
# following 2 steps - need to install the frontend dependencies for the eslint/prettier/stylelint steps
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- uses: pre-commit/action@v3.0.0
lint-test-and-build-frontend:
name: "Lint, test, and build frontend"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Build frontend (will run linter and tests)
working-directory: grafana-plugin
run: yarn build
test-technical-documentation:
name: "Test technical documentation"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: "actions/checkout@v3"
- name: "Build website"
# -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside
# that no refs to external content can be used as these refs will not resolve in the
# docs-base image.
run: |
docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
lint-migrations-backend-mysql-rabbitmq:
name: "Lint database migrations"
runs-on: ubuntu-latest
env:
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
ports:
- 3306:3306
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Lint migrations
working-directory: engine
run: |
pip install -r requirements.txt
python manage.py lintmigrations
unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
ports:
- 3306:3306
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
apt-get update && apt-get install -y netcat
pip install -r requirements.txt
./wait_for_test_mysql_start.sh && pytest -x
unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: postgresql
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
postgresql_test:
image: postgres:14.4
env:
POSTGRES_DB: oncall_local_dev
POSTGRES_PASSWORD: local_dev_pwd
ports:
- 5432:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
pip install -r requirements.txt
pytest -x
unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: sqlite3
BROKER_TYPE: redis
REDIS_URI: redis://localhost:6379
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
redis_test:
image: redis:7.0.5
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
apt-get update && apt-get install -y netcat
pip install -r requirements.txt
pytest -x
unit-test-pd-migrator:
name: "Unit tests - PagerDuty Migrator"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: tools/pagerduty-migrator/requirements.txt
- name: Unit Test PD Migrator
working-directory: tools/pagerduty-migrator
run: |
pip install -r requirements.txt
pytest -x
end-to-end-tests:
runs-on: ubuntu-latest
name: "End to end tests - Grafana: ${{ matrix.grafana-image-tag }}"
strategy:
matrix:
grafana-image-tag:
- 9.2.6
- main
- latest
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Use cached plugin frontend build
id: cache-plugin-frontend
uses: actions/cache@v3
with:
path: grafana-plugin/dist
key: ${{ runner.os }}-plugin-frontend-${{ hashFiles('grafana-plugin/src/**/*', 'grafana-plugin/yarn.lock') }}
- name: Build plugin frontend
if: steps.cache-plugin-frontend.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn build:dev
- name: Set up Docker Buildx # We need this step for docker caching
uses: docker/setup-buildx-action@v2
- name: Build engine Docker image locally # using github actions docker cache
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
load: true
tags: oncall/engine:latest
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/oncall-engine.tar
- name: Load engine Docker image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
# spin up 2 engine, 2 celery, and 2 grafana pods, this will allow us to parralelize the integration tests
# and complete them much faster by using multiple test processes
# With just 1 engine/celery/grafana pod, the backend crawls to a halt when there is > 1 parallelized integration
# test process
#
# by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built
# OnCall plugin rather than the latest published version
# the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir
# see ./helm/kind.yml for more details
- name: Install helm chart
run: |
helm install helm-testing \
--values ./helm/simple.yml \
--values ./helm/values-local-image.yml \
--set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \
--set engine.replicaCount=1 \
--set celery.replicaCount=1 \
--set celery.worker_beat_enabled="False" \
--set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \
--set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \
--set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \
--set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \
--set grafana.replicas=1 \
--set grafana.image.tag=${{ matrix.grafana-image-tag }} \
--set grafana.env.GF_SECURITY_ADMIN_USER=oncall \
--set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \
--set grafana.env.GF_FEATURE_TOGGLES_ENABLE=topnav \
--set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \
--set-json "grafana.plugins=[]" \
--set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \
--set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \
./helm/oncall
# helpful reference for properly caching the playwright binaries/dependencies
# https://playwrightsolutions.com/playwright-github-action-to-cache-the-browser-binaries/
- name: Get installed Playwright version
id: playwright-version
working-directory: grafana-plugin
run: echo "PLAYWRIGHT_VERSION=$(cat ./package.json | jq -r '.devDependencies["@playwright/test"]')" >> $GITHUB_ENV
- name: Cache Playwright binaries/dependencies
id: playwright-cache
uses: actions/cache@v3
with:
path: "~/.cache/ms-playwright"
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright binaries/dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
# if more browsers are added, will need to modify the "npx playwright install" command
# https://stackoverflow.com/questions/65900299/install-single-dependency-from-package-json-with-yarn
run: |
yarn add "@playwright/test@${{ env.PLAYWRIGHT_VERSION }}"
npx playwright install --with-deps chromium firefox
npx playwright install-deps
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
- name: Run Integration Tests
env:
# BASE_URL represents what is accessed via a browser
BASE_URL: http://localhost:30002/grafana
# ONCALL_API_URL is what is configured in the plugin configuration form
# it is what the grafana container uses to communicate with the OnCall backend
#
# 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container
# hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped
# to the OnCall API
ONCALL_API_URL: http://172.17.0.1:30001
GRAFANA_USERNAME: oncall
GRAFANA_PASSWORD: oncall
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
working-directory: ./grafana-plugin
# -x = exit command after first failing test
run: yarn test:integration -x
# always spit out the engine and celery logs, AFTER the e2e tests have completed
# can be helpful for debugging failing/flaky tests
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: failure()
with:
important-workloads: "deploy/helm-testing-oncall-engine deploy/helm-testing-oncall-celery"
- uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report-grafana-${{ matrix.grafana-image-tag }}
path: ./grafana-plugin/playwright-report/
retention-days: 30

View file

@ -1,4 +1,4 @@
name: "publish-technical-documentation-next"
name: "Publish Technical Documentation (next)"
on:
push:

View file

@ -1,4 +1,4 @@
name: "publish-technical-documentation-release"
name: "Publish Technical Documentation (release)"
on:
push:

View file

@ -17,16 +17,21 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- uses: snyk/actions/setup@master
- name: Install Dependencies
run: |
pip install -r engine/requirements.txt
cd grafana-plugin/
yarn --network-timeout 500000
- name: Install backend dependencies
working-directory: engine
run: pip install -r requirements.txt
- name: Install frontend dependencies
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Run Snyk
continue-on-error: true
run: snyk test --all-projects --severity-threshold=high

View file

@ -21,7 +21,7 @@ else:
}
if BROKER_TYPE == BrokerTypes.RABBITMQ:
CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672"
CELERY_BROKER_URL = RABBITMQ_URI
elif BROKER_TYPE == BrokerTypes.REDIS:
CELERY_BROKER_URL = REDIS_URI

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
until nc -z -v -w30 mysql_test 3306
until nc -z -v -w30 "${DATABASE_HOST:=mysql_test}" 3306
do
echo "Waiting for database connection..."
sleep 1

View file

@ -1,15 +1,10 @@
import { test } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { createOnCallSchedule } from '../utils/schedule';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('we can create an oncall schedule + receive an alert', async ({ page }) => {
const escalationChainName = generateRandomValue();
const integrationName = generateRandomValue();

View file

@ -1,5 +1,4 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { GRAFANA_USERNAME } from '../utils/constants';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
@ -7,10 +6,6 @@ import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { waitForSms } from '../utils/phone';
import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
// TODO: enable once we've signed up for a MailSlurp account to receieve SMSes
test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => {
const escalationChainName = generateRandomValue();

View file

@ -1,12 +1,7 @@
import { test, expect, Page } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createEscalationChain } from '../utils/escalationChain';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
const assertEscalationChainSearchWorks = async (
page: Page,
searchTerm: string,
@ -20,18 +15,18 @@ const assertEscalationChainSearchWorks = async (
await expect(page.getByTestId('escalation-chains-list')).toHaveText(escalationChainFullName);
};
test('searching allows case-insensitive partial matches', async ({ page }) => {
// TODO: add tests for the new filtering. Commented out as this search doesn't exist anymore
test.skip('searching allows case-insensitive partial matches', async ({ page }) => {
const escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`;
// const [firstHalf, secondHalf] = escalationChainName.split(' ');
const [firstHalf, secondHalf] = escalationChainName.split(' ');
await createEscalationChain(page, escalationChainName);
// Commented as this search doesn't exist anymore TODO: add tests for the new filtering
// await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
//
// await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
});

View file

@ -1,6 +1,39 @@
import { chromium, FullConfig, expect } from '@playwright/test';
import { chromium, FullConfig, expect, Page } from '@playwright/test';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME } from './utils/constants';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants';
import { clickButton, getInputByName } from './utils/forms';
import { goToGrafanaPage } from './utils/navigation';
/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
export const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}
/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
await goToGrafanaPage(page, '/plugins/grafana-oncall-app');
await page.waitForSelector('text=Configure Grafana OnCall');
/**
* we may need to fill in the OnCall API URL if it is not set in the process.env
* of the frontend build
*/
const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl');
const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0;
if (!pluginIsAutoConfigured) {
await onCallApiUrlInput.fill(ONCALL_API_URL);
await clickButton({ page, buttonText: 'Connect' });
}
// wait for the "Connected to OnCall" message to know that everything is properly configured
await expect(page.getByTestId('status-message-block')).toHaveText(/Connected to OnCall.*/);
};
/**
* Borrowed from our friends on the Incident team
@ -20,6 +53,11 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
expect(res.ok()).toBeTruthy();
await browserContext.storageState({ path: './storageState.json' });
// make sure the plugin has been configured
const page = await browserContext.newPage();
await configureOnCallPlugin(page);
await browserContext.close();
};

View file

@ -1,11 +1,6 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { openCreateIntegrationModal } from '../utils/integrations';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('integrations have unique names', async ({ page }) => {
await openCreateIntegrationModal(page);

View file

@ -1,13 +1,8 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { clickButton, generateRandomValue } from '../utils/forms';
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
import dayjs from 'dayjs';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('default dates in override creation modal are correct', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);

View file

@ -1,12 +1,7 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createOnCallSchedule } from '../utils/schedule';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('check schedule quality for simple 1-user schedule', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);
@ -14,6 +9,10 @@ test('check schedule quality for simple 1-user schedule', async ({ page }) => {
await expect(page.locator('div[class*="ScheduleQuality"]')).toHaveText('Quality: Great');
await page.hover('div[class*="ScheduleQuality"]');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText('Schedule has no gaps');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText('Schedule is perfectly balanced');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText(
'Schedule has no gaps'
);
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText(
'Schedule is perfectly balanced'
);
});

View file

@ -1,38 +0,0 @@
import type { Page } from '@playwright/test';
import { ONCALL_API_URL, IS_OPEN_SOURCE } from './constants';
import { clickButton, getInputByName } from './forms';
import { goToGrafanaPage } from './navigation';
/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
export const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}
/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
await goToGrafanaPage(page, '/plugins/grafana-oncall-app');
await page.waitForSelector('text=Configure Grafana OnCall');
/**
* we may need to fill in the OnCall API URL if it is not set in the process.env
* of the frontend build
*/
const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl');
const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0;
if (!pluginIsAutoConfigured) {
await onCallApiUrlInput.fill(ONCALL_API_URL);
await clickButton({ page, buttonText: 'Connect' });
}
/**
* wait for the page to be refreshed and the icon to show up, this means the plugin
* has been successfully configured
*/
await page.waitForSelector('div.scrollbar-view img[src*="grafana-oncall-app/img/logo.svg"]');
};

View file

@ -1,4 +1,4 @@
import { Page } from '@playwright/test';
import { expect, Page } from '@playwright/test';
import { clickButton, fillInInput, selectDropdownValue } from './forms';
import { goToOnCallPage } from './navigation';
@ -22,6 +22,14 @@ export const createEscalationChain = async (
// go to the escalation chains page
await goToOnCallPage(page, 'escalations');
/**
* wait for Esclation Chains page to fully load. this is because this can change which "New Escalation Chain"
* button is present
* ie. the one on the left hand side in the list vs the one in the center when no escalation chains exist
*/
await page.getByTestId('page-title').locator('text=Escalation Chains').waitFor({ state: 'visible' });
await page.locator('text=Loading...').waitFor({ state: 'detached' });
// open the create escalation chain modal
(await page.waitForSelector('text=New Escalation Chain')).click();
@ -30,7 +38,7 @@ export const createEscalationChain = async (
// submit the form and wait for it to be created
await clickButton({ page, buttonText: 'Create' });
await page.waitForSelector(`text=${escalationChainName}`);
await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName);
if (!escalationStep || !escalationStepValue) {
return;

View file

@ -54,7 +54,7 @@ const openSelect = async ({
placeholderText,
selectType,
startingLocator,
}: SelectDropdownValueArgs): Promise<void> => {
}: SelectDropdownValueArgs): Promise<Locator> => {
/**
* we currently mix three different dropdown components in the UI..
* so we need to support all of them :(
@ -73,6 +73,8 @@ const openSelect = async ({
const selectElement: Locator = (startingLocator || page).locator(selector);
await selectElement.waitFor({ state: 'visible' });
await selectElement.click();
return selectElement;
};
/**
@ -85,7 +87,15 @@ const chooseDropdownValue = async ({ page, value, optionExactMatch = true }: Sel
page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click();
export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise<void> => {
await openSelect(args);
const selectElement = await openSelect(args);
/**
* use the select search to filter down the options
* TODO: get rid of the slice when we fix the GSelect component..
* without slicing this would fire off an API request for every key-stroke
*/
await selectElement.type(args.value.slice(0, 5));
await chooseDropdownValue(args);
};

View file

@ -59,7 +59,7 @@
"@grafana/eslint-config": "^5.0.0",
"@grafana/toolkit": "^9.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.28.0",
"@playwright/test": "^1.32.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "12",
"@testing-library/user-event": "^14.4.3",

View file

@ -14,7 +14,8 @@ const config: PlaywrightTestConfig = {
testDir: './integration-tests',
globalSetup: './integration-tests/globalSetup.ts',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
// TODO: set this back to 60 when GSelect component is refactored
timeout: 90 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
@ -27,8 +28,10 @@ const config: PlaywrightTestConfig = {
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
retries: process.env.CI ? 1 : 0,
// TODO: when GSelect component is refactored, run using 3 workers
// locally use one worker, on CI use 3
// workers: process.env.CI ? 3 : 1,
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',

View file

@ -23,7 +23,11 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode {
{/* Render alerts at the top */}
<Alerts />
<Header backendLicense={store.backendLicense} />
{pages[page]?.text && !pages[page]?.hideTitle && <h3 className="page-title">{pages[page].text}</h3>}
{pages[page]?.text && !pages[page]?.hideTitle && (
<h3 className="page-title" data-testid="page-title">
{pages[page].text}
</h3>
)}
{props.children}
</RealPluginPage>
);

View file

@ -297,7 +297,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={notify_schedule}
onChange={this._getOnChangeHandler('notify_schedule')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[scheduleStore.items[item.value].team];
return (
@ -351,7 +350,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={custom_button_trigger}
onChange={this._getOnChangeHandler('custom_button_trigger')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
return (

View file

@ -9,6 +9,7 @@ import { observer } from 'mobx-react';
import { useStore } from 'state/useStore';
import styles from './GSelect.module.css';
// import { debounce } from 'lodash';
const cx = cn.bind(styles);
@ -30,7 +31,6 @@ interface GSelectProps {
showWarningIfEmptyValue?: boolean;
showError?: boolean;
nullItemName?: string;
fromOrganization?: boolean;
filterOptions?: (id: any) => boolean;
dropdownRender?: (menu: ReactElement) => ReactElement;
getOptionLabel?: <T>(item: SelectableValue<T>) => React.ReactNode;
@ -61,7 +61,6 @@ const GSelect = observer((props: GSelectProps) => {
showWarningIfEmptyValue = false,
getDescription,
filterOptions,
// fromOrganization,
width = null,
icon = null,
} = props;
@ -89,6 +88,11 @@ const GSelect = observer((props: GSelectProps) => {
[model, onChange]
);
/**
* without debouncing this function when search is available
* we risk hammering the API endpoint for every single key stroke
* some context on 250ms as the choice here - https://stackoverflow.com/a/44755058/3902555
*/
const loadOptions = (query: string) => {
return model.updateItems(query).then(() => {
const searchResult = model.getSearchResult(query);
@ -106,6 +110,9 @@ const GSelect = observer((props: GSelectProps) => {
});
};
// TODO: why doesn't this work properly?
// const loadOptions = debounce(_loadOptions, showSearch ? 250 : 0);
const values = isMulti
? (value ? (value as string[]) : [])
.filter((id) => id in model.items)

View file

@ -287,7 +287,12 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
return (
<>
<Block withBackground className={cx('header')}>
<Text size="large" editable onTextChange={this.handleEscalationChainNameChange}>
<Text
size="large"
editable
onTextChange={this.handleEscalationChainNameChange}
data-testid="escalation-chain-name"
>
{escalationChain.name}
</Text>
<div className={cx('buttons')}>

View file

@ -2603,13 +2603,15 @@
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586"
integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg==
"@playwright/test@^1.28.0":
version "1.28.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.0.tgz#8de83f9d2291bba3f37883e33431b325661720d9"
integrity sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==
"@playwright/test@^1.32.0":
version "1.32.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.0.tgz#0cc4c179e62995cc123adb12fdfaa093fed282c4"
integrity sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==
dependencies:
"@types/node" "*"
playwright-core "1.28.0"
playwright-core "1.32.0"
optionalDependencies:
fsevents "2.3.2"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
@ -7083,7 +7085,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2:
fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@ -10347,10 +10349,10 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
playwright-core@1.28.0:
version "1.28.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.0.tgz#61df5c714f45139cca07095eccb4891e520e06f2"
integrity sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==
playwright-core@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.0.tgz#730c2d1988d30377480b925aaa6c1b1e2442d67e"
integrity sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==
please-upgrade-node@^3.2.0:
version "3.2.0"