Remove monitoring app (#784)
* Remove monitoring app * remove monitoring cicd
This commit is contained in:
parent
70cb741f88
commit
44333a6784
85 changed files with 0 additions and 6881 deletions
|
@ -4,7 +4,6 @@
|
||||||
"saleor-app-emails-and-messages": patch
|
"saleor-app-emails-and-messages": patch
|
||||||
"saleor-app-data-importer": patch
|
"saleor-app-data-importer": patch
|
||||||
"saleor-app-products-feed": patch
|
"saleor-app-products-feed": patch
|
||||||
"saleor-app-monitoring": patch
|
|
||||||
"@saleor/apps-shared": patch
|
"@saleor/apps-shared": patch
|
||||||
"saleor-app-invoices": patch
|
"saleor-app-invoices": patch
|
||||||
"saleor-app-klaviyo": patch
|
"saleor-app-klaviyo": patch
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"saleor-app-emails-and-messages": patch
|
"saleor-app-emails-and-messages": patch
|
||||||
"saleor-app-data-importer": patch
|
"saleor-app-data-importer": patch
|
||||||
"saleor-app-products-feed": patch
|
"saleor-app-products-feed": patch
|
||||||
"saleor-app-monitoring": patch
|
|
||||||
"@saleor/apps-shared": patch
|
"@saleor/apps-shared": patch
|
||||||
"saleor-app-invoices": patch
|
"saleor-app-invoices": patch
|
||||||
"saleor-app-klaviyo": patch
|
"saleor-app-klaviyo": patch
|
||||||
|
|
21
.github/dependabot.yaml
vendored
21
.github/dependabot.yaml
vendored
|
@ -88,27 +88,6 @@ updates:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "[skip ci]"
|
prefix: "[skip ci]"
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: apps/monitoring
|
|
||||||
open-pull-requests-limit: 1
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
commit-message:
|
|
||||||
prefix: "[skip ci]"
|
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: apps/monitoring/backend
|
|
||||||
open-pull-requests-limit: 1
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
commit-message:
|
|
||||||
prefix: "[skip ci]"
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: apps/monitoring/backend
|
|
||||||
open-pull-requests-limit: 1
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
commit-message:
|
|
||||||
prefix: "[skip ci]"
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: apps/products-feed
|
directory: apps/products-feed
|
||||||
open-pull-requests-limit: 1
|
open-pull-requests-limit: 1
|
||||||
|
|
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
|
@ -1,7 +1,6 @@
|
||||||
"App: Data Importer": "apps/data-importer/**/*"
|
"App: Data Importer": "apps/data-importer/**/*"
|
||||||
"App: Invoices": "apps/invoices/**/*"
|
"App: Invoices": "apps/invoices/**/*"
|
||||||
"App: Klaviyo": "apps/klaviyo/**/*"
|
"App: Klaviyo": "apps/klaviyo/**/*"
|
||||||
"App: Monitoring": "apps/monitoring/**/*"
|
|
||||||
"App: Product Feed": "apps/products-feed/**/*"
|
"App: Product Feed": "apps/products-feed/**/*"
|
||||||
"App: Search": "apps/search/**/*"
|
"App: Search": "apps/search/**/*"
|
||||||
"App: Slack": "apps/slack/**/*"
|
"App: Slack": "apps/slack/**/*"
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
name: Publish image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- apps/monitoring/backend/**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
env:
|
|
||||||
AWS_REGION: eu-west-1
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
with:
|
|
||||||
platforms: all
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_APPS_STAGING_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_APPS_STAGING_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: ${{ env.AWS_REGION }}
|
|
||||||
|
|
||||||
- id: ecr-login
|
|
||||||
name: Login to Amazon ECR
|
|
||||||
uses: aws-actions/amazon-ecr-login@v1
|
|
||||||
with:
|
|
||||||
registries: ${{ secrets.AWS_ECR_ACCOUNT }}
|
|
||||||
|
|
||||||
- name: Evaluate image tags
|
|
||||||
run: |
|
|
||||||
IMAGE_REPOSITORY=${{ steps.ecr-login.outputs.registry }}/${{ secrets.ECR_REPOSITORY }}
|
|
||||||
BRANCH_IMAGE_TAG=${{ github.ref_name }}
|
|
||||||
UNIQUE_IMAGE_TAG=${BRANCH_IMAGE_TAG}-$(git rev-parse --short HEAD)
|
|
||||||
|
|
||||||
IMAGE_TAGS=${IMAGE_REPOSITORY}:${BRANCH_IMAGE_TAG},${IMAGE_REPOSITORY}:${UNIQUE_IMAGE_TAG}
|
|
||||||
|
|
||||||
echo "UNIQUE_IMAGE_TAG=${UNIQUE_IMAGE_TAG}" >> $GITHUB_ENV
|
|
||||||
echo "IMAGE_TAGS=${IMAGE_TAGS}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
timeout-minutes: 20
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: ./apps/monitoring/backend
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.IMAGE_TAGS }}
|
|
||||||
target: prod
|
|
||||||
cache-from: type=gha,scope=buildkit-master
|
|
||||||
cache-to: type=gha,scope=buildkit-master
|
|
||||||
|
|
||||||
# - name: Configure GitHub credentials
|
|
||||||
# run: |
|
|
||||||
# GITHUB_TOKEN=$( \
|
|
||||||
# curl --request GET --url ${{ secrets.VAULT_URL}} --header "Authorization: JWT ${{ secrets.VAULT_JWT }}" | jq -r .token \
|
|
||||||
# )
|
|
||||||
# echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: Trigger Helm deployment
|
|
||||||
# run: |
|
|
||||||
# gh api /repos/saleor/saleor-cloud-deployments/dispatches \
|
|
||||||
# --input - <<< '{
|
|
||||||
# "event_type": "deploy-app-monitoring-staging",
|
|
||||||
# "client_payload": {
|
|
||||||
# "image_tag": "${{ env.UNIQUE_IMAGE_TAG }}"
|
|
||||||
# }
|
|
||||||
# }'
|
|
33
.github/workflows/unit-tests-monitoring.yml
vendored
33
.github/workflows/unit-tests-monitoring.yml
vendored
|
@ -1,33 +0,0 @@
|
||||||
name: "App: Monitoring backend tests"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "apps/monitoring/backend/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unit_test:
|
|
||||||
name: Unit tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: apps/monitoring/backend
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install poetry
|
|
||||||
run: pipx install poetry
|
|
||||||
- name: Setup python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.10"
|
|
||||||
cache: poetry
|
|
||||||
- name: Install dependencies
|
|
||||||
run: poetry install
|
|
||||||
- name: Run unit tests
|
|
||||||
run: poetry run pytest
|
|
||||||
- name: Run black
|
|
||||||
run: poetry run black .
|
|
||||||
- name: Run ruff
|
|
||||||
run: poetry run ruff .
|
|
||||||
- name: Run mypy
|
|
||||||
run: poetry run mypy .
|
|
|
@ -1 +0,0 @@
|
||||||
MONITORING_APP_API_URL=
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"extends": ["saleor"]
|
|
||||||
}
|
|
125
apps/monitoring/.gitignore
vendored
125
apps/monitoring/.gitignore
vendored
|
@ -1,125 +0,0 @@
|
||||||
# Environments
|
|
||||||
.venv
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# Ruff
|
|
||||||
.ruff_cache
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
.fileApl.json
|
|
|
@ -1,19 +0,0 @@
|
||||||
schema: backend/monitoring/schema.graphql
|
|
||||||
documents: [src/**/*.tsx]
|
|
||||||
extensions:
|
|
||||||
codegen:
|
|
||||||
overwrite: true
|
|
||||||
generates:
|
|
||||||
generated/graphql.ts:
|
|
||||||
config:
|
|
||||||
dedupeFragments: true
|
|
||||||
plugins:
|
|
||||||
- typescript
|
|
||||||
- typescript-operations
|
|
||||||
- typescript-urql:
|
|
||||||
documentVariablePrefix: "Untyped"
|
|
||||||
fragmentVariablePrefix: "Untyped"
|
|
||||||
- typed-document-node
|
|
||||||
generated/schema.graphql:
|
|
||||||
plugins:
|
|
||||||
- schema-ast
|
|
|
@ -1,6 +0,0 @@
|
||||||
.next
|
|
||||||
saleor/api.tsx
|
|
||||||
pnpm-lock.yaml
|
|
||||||
graphql/schema.graphql
|
|
||||||
generated
|
|
||||||
backend
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"singleQuote": false,
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
|
@ -1,189 +0,0 @@
|
||||||
# saleor-app-monitoring
|
|
||||||
|
|
||||||
## 1.1.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 47102ba: Added additional ENV variables (see each app's .env.example), that can overwrite app base URL. This change allows easy apps development using Docker
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 2d77bca: Updated Next.js to 13.4.8
|
|
||||||
- 6299e06: Update @saleor/app-sdk to 0.41.0
|
|
||||||
- Updated dependencies [2d77bca]
|
|
||||||
- Updated dependencies [6299e06]
|
|
||||||
- @saleor/apps-shared@1.7.3
|
|
||||||
- @saleor/apps-ui@1.1.3
|
|
||||||
- @saleor/react-hook-form-macaw@0.2.0
|
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
### Major Changes
|
|
||||||
|
|
||||||
- 3bd7e3f: Updated App's UI to the new Macaw. Simplified the view and removed unnecessary not-implemented providers.
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 1dead1e: Included dedicated logo and attached it to App's manifest. From Saleor 3.15 the logo will be visible in the Dashboard during and after installation.
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 860bac4: Updated @saleor/app-sdk to 0.40.1
|
|
||||||
- a1ad70e: Updated configuration and dependencies of GraphQL client - urql.
|
|
||||||
All applications use now unified config for creating the client. Also unused related packages has been removed.
|
|
||||||
- cb6ee29: Updated dependencies
|
|
||||||
- Updated dependencies [f96563f]
|
|
||||||
- Updated dependencies [f96563f]
|
|
||||||
- Updated dependencies [860bac4]
|
|
||||||
- Updated dependencies [a1ad70e]
|
|
||||||
- Updated dependencies [cb6ee29]
|
|
||||||
- Updated dependencies [a1ad70e]
|
|
||||||
- @saleor/react-hook-form-macaw@0.2.0
|
|
||||||
- @saleor/apps-ui@1.1.2
|
|
||||||
- @saleor/apps-shared@1.7.2
|
|
||||||
|
|
||||||
## 0.6.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- a8834a1: Unified graphql version to 16.6
|
|
||||||
- a8834a1: Unified graphql codegen packages
|
|
||||||
- a8834a1: Removed unnecessary duplicated dependencies from apps and moved them to shared and root (types, eslint rules)
|
|
||||||
- a8834a1: Updated dev dependencies - Typescript, Eslint and Turborepo
|
|
||||||
- Updated dependencies [a8834a1]
|
|
||||||
- Updated dependencies [a8834a1]
|
|
||||||
- Updated dependencies [a8834a1]
|
|
||||||
- Updated dependencies [a8834a1]
|
|
||||||
- @saleor/apps-shared@1.7.1
|
|
||||||
|
|
||||||
## 0.6.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 0c2fc65: Update dev dependencies - Vite and Vitest. These changes will not affect runtime Apps, but can affect tests and builds
|
|
||||||
- Updated dependencies [0c2fc65]
|
|
||||||
- Updated dependencies [b75a664]
|
|
||||||
- @saleor/apps-shared@1.7.0
|
|
||||||
|
|
||||||
## 0.6.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6e69f4f: Update app-sdk to 0.39.1
|
|
||||||
- Updated dependencies [6e69f4f]
|
|
||||||
- @saleor/apps-shared@1.6.1
|
|
||||||
|
|
||||||
## 0.6.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [23b5c70]
|
|
||||||
- @saleor/apps-shared@1.6.0
|
|
||||||
|
|
||||||
## 0.6.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- c406318: Updated dep @saleor/app-sdk to 0.38.0
|
|
||||||
- Updated dependencies [c406318]
|
|
||||||
- @saleor/apps-shared@1.5.1
|
|
||||||
|
|
||||||
## 0.6.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 8b22b1c: Restored Pino logger packages to each app, to fix failing logs in development. Also updated .env.example to contain up to date APP_LOG_LEVEL variable
|
|
||||||
|
|
||||||
## 0.6.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 830cfe9: Changed APP_DEBUG env to APP_LOG_LEVEL
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [830cfe9]
|
|
||||||
- @saleor/apps-shared@1.5.0
|
|
||||||
|
|
||||||
## 0.5.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 57f6d41: Updated Manifest to contain up to date support, privacy, homepage and author fields
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 2c0df91: Added lint:fix script, so `eslint --fix` can be run deliberately
|
|
||||||
- e167e72: Update next.js to 13.3.0
|
|
||||||
- 74174c4: Updated @saleor/app-sdk to 0.37.3
|
|
||||||
- 2e51890: Update next.js to 13.3.0
|
|
||||||
- 2e51890: Update @saleor/app-sdk to 0.37.2
|
|
||||||
- 2e51890: Use useDashboardNotification hook from shared package, instead of direct AppBridge usage
|
|
||||||
- Updated dependencies [2c0df91]
|
|
||||||
- Updated dependencies [e167e72]
|
|
||||||
- Updated dependencies [74174c4]
|
|
||||||
- Updated dependencies [2e51890]
|
|
||||||
- Updated dependencies [2e51890]
|
|
||||||
- Updated dependencies [2e51890]
|
|
||||||
- @saleor/apps-shared@1.4.0
|
|
||||||
|
|
||||||
## 0.4.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- eca52ad: Replace "export default" with named exports
|
|
||||||
- @saleor/apps-shared@1.3.0
|
|
||||||
|
|
||||||
## 0.4.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 7cb3b89: Added "author" field to the Manifest, set it to Saleor Commerce, so Dashboard can display it too
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 7cb3b89: Replace apps to avoid AppPermission (use Permission for client permissions) and authData.domain (use saleorApiUrl)
|
|
||||||
- 7cb3b89: Updated @saleor/app-sdk to 0.37.1
|
|
||||||
|
|
||||||
## 0.3.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- e93a4dc: Updated GraphQL Code Generator package
|
|
||||||
|
|
||||||
## 0.3.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- dca82bb: Update app-sdk to pre-0.34.0. Update Async Webhooks to use new API
|
|
||||||
|
|
||||||
## 0.3.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 2755ed2: Added extra padding on top of the app so it has some space between content and dashboard header
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 2d23480: Remove TitleBar component from apps, because it is moved to Dashboard, outside of iframe context
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [2d23480]
|
|
||||||
- @saleor/apps-shared@1.3.0
|
|
||||||
|
|
||||||
## 0.2.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 289b42f: Breaking change for app maintainers: VercelAPL can no longer be set for the app since it's deprecated and will be removed in app-sdk 0.30.0. As a replacement, we recommend using Upstash APL or implementing your own.
|
|
||||||
Read more about APLs: https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [5fc88ed]
|
|
||||||
- @saleor/apps-shared@1.2.0
|
|
|
@ -1,78 +0,0 @@
|
||||||
# Saleor Monitoring app
|
|
||||||
|
|
||||||
❗️NOTE: This is Alpha version of the app.❗️
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
### Start Monitoring backend
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker-compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
It is beneficial to run this command in a separate terminal tab to observe backend logs easily.
|
|
||||||
|
|
||||||
By default, backend will run at `localhost:5001` with:
|
|
||||||
|
|
||||||
- Manifest at `/manifest`
|
|
||||||
- Graphql Playground at `/graphql`
|
|
||||||
- OpenApi viewer at `/docs`
|
|
||||||
|
|
||||||
### Develop frontend:
|
|
||||||
|
|
||||||
Installing dependencies with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm i
|
|
||||||
```
|
|
||||||
|
|
||||||
Running dev server
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The frontend app will run at `localhost:3000`.
|
|
||||||
By default, it acts as a proxy and redirects all unhandled requests to the backend (configured by `MONITORING_APP_API_URL` env).
|
|
||||||
This way, all frontend and backend endpoints are accessible at `http://localhost:3000`
|
|
||||||
|
|
||||||
### Test with Saleor
|
|
||||||
|
|
||||||
Expose `http://localhost:3000` using a tunnel and use `https://your.tunnel/manifest` manifest URL to install `Monitoring` app
|
|
||||||
|
|
||||||
### Graphql Playground
|
|
||||||
|
|
||||||
To use Graphql Playground, `Monitoring` app needs to be installed in Saleor, and HTTP headers must be set:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"authorization-bearer": "token",
|
|
||||||
"saleor-api-url": "https://my-env.saleor.cloud/graphql/"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing DataDog integration
|
|
||||||
|
|
||||||
Set `MOCK_DATADOG_CLIENT` env to `True`
|
|
||||||
|
|
||||||
Use these credentials sets to test DataDog integration:
|
|
||||||
|
|
||||||
Working credentials:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"site": "US1",
|
|
||||||
"apiKey": "156e22d50c4e8b6816e1fd4794d3fd8c"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Credentials that validate but generate an error while sending events
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"site": "EU1",
|
|
||||||
"apiKey": "156e22d50c4e8b6816e1fd4794d3fd8c"
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1 +0,0 @@
|
||||||
**/.fileApl.json
|
|
|
@ -1,28 +0,0 @@
|
||||||
FROM python:3.10 as build
|
|
||||||
|
|
||||||
RUN pip install poetry'>=1.3.2,<1.4.0'
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY pyproject.toml poetry.lock /app/
|
|
||||||
|
|
||||||
RUN POETRY_VIRTUALENVS_CREATE=false poetry install --no-cache --only main
|
|
||||||
|
|
||||||
FROM python:3.10-slim as prod
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
|
|
||||||
COPY --from=build /usr/local/bin/ /usr/local/bin/
|
|
||||||
COPY --from=build /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY monitoring/ /app/monitoring/
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["uvicorn", "monitoring.app:app", "--host", "0.0.0.0", "--port", "80", "--no-access-log", "--forwarded-allow-ips", "*"]
|
|
||||||
|
|
||||||
FROM prod as dev
|
|
||||||
|
|
||||||
COPY pyproject.toml poetry.lock /app/
|
|
||||||
RUN POETRY_VIRTUALENVS_CREATE=false poetry install --no-cache --only dev
|
|
|
@ -1 +0,0 @@
|
||||||
__version__ = "0.1.0"
|
|
|
@ -1,15 +0,0 @@
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
uvicorn.run(
|
|
||||||
"monitoring.app:app",
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=5001,
|
|
||||||
reload=True,
|
|
||||||
forwarded_allow_ips="*",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,93 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ariadne import (
|
|
||||||
MutationType,
|
|
||||||
ObjectType,
|
|
||||||
QueryType,
|
|
||||||
convert_kwargs_to_snake_case,
|
|
||||||
load_schema_from_path,
|
|
||||||
make_executable_schema,
|
|
||||||
snake_case_fallback_resolvers,
|
|
||||||
)
|
|
||||||
from ariadne.asgi import GraphQL
|
|
||||||
from graphql import GraphQLResolveInfo
|
|
||||||
|
|
||||||
from .deps import ApiDependencies
|
|
||||||
from .schema import DatadogConfig, DatadogCredentials
|
|
||||||
from .settings import settings
|
|
||||||
|
|
||||||
base_dir = Path(__file__).resolve().parent
|
|
||||||
type_defs = load_schema_from_path(str(base_dir / "schema.graphql"))
|
|
||||||
query = QueryType()
|
|
||||||
datadog_config = ObjectType("DatadogConfig")
|
|
||||||
mutation = MutationType()
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_context(info: GraphQLResolveInfo) -> ApiDependencies:
|
|
||||||
return info.context["request"].state.api_context
|
|
||||||
|
|
||||||
|
|
||||||
@datadog_config.field("credentials")
|
|
||||||
def resolve_datadog_credentials(datadog: DatadogConfig, *_):
|
|
||||||
return {
|
|
||||||
"site": datadog.credentials.site,
|
|
||||||
"api_key_last_4": datadog.credentials.api_key[-4:],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@query.field("integrations")
|
|
||||||
async def resolve_integrations(_, info):
|
|
||||||
context = get_api_context(info)
|
|
||||||
metadata = await context.manager.get_metadata()
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
|
|
||||||
@mutation.field("updateDatadogConfig")
|
|
||||||
@convert_kwargs_to_snake_case
|
|
||||||
async def resolve_update_datadog(_, info, input):
|
|
||||||
context = get_api_context(info)
|
|
||||||
metadata = await context.manager.get_metadata()
|
|
||||||
|
|
||||||
if creds := input.get("credentials", None):
|
|
||||||
creds = DatadogCredentials(site=creds["site"], api_key=creds["api_key"])
|
|
||||||
if not await context.datadog_client.validate_credentials(creds):
|
|
||||||
return {
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"field": "credentials",
|
|
||||||
"message": f"Invalid datadog apiKey for site {creds.site}",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create new configuration
|
|
||||||
if metadata.datadog is None:
|
|
||||||
if creds is None:
|
|
||||||
return {"errors": [{"message": "No DataDog config to update"}]}
|
|
||||||
metadata.datadog = DatadogConfig(credentials=creds)
|
|
||||||
|
|
||||||
if "active" in input:
|
|
||||||
metadata.datadog.active = input["active"]
|
|
||||||
if creds:
|
|
||||||
metadata.datadog.credentials = creds
|
|
||||||
metadata.datadog.error = None
|
|
||||||
|
|
||||||
await context.manager.save_private_metadata(metadata)
|
|
||||||
return {"datadog": metadata.datadog, "errors": []}
|
|
||||||
|
|
||||||
|
|
||||||
@mutation.field("deleteDatadogConfig")
|
|
||||||
async def resolve_delete_datadog(_, info):
|
|
||||||
context = get_api_context(info)
|
|
||||||
metadata = await context.manager.get_metadata()
|
|
||||||
if metadata.datadog is None:
|
|
||||||
return {"errors": [{"message": "No DataDog config to delete"}]}
|
|
||||||
await context.manager.delete_private_metadata("datadog")
|
|
||||||
return {"datadog": metadata.datadog, "errors": []}
|
|
||||||
|
|
||||||
|
|
||||||
schema = make_executable_schema(
|
|
||||||
type_defs, query, datadog_config, mutation, snake_case_fallback_resolvers
|
|
||||||
)
|
|
||||||
graphQL = GraphQL(schema, debug=settings.debug)
|
|
||||||
graphql_app = graphQL.http_handler
|
|
|
@ -1,12 +0,0 @@
|
||||||
from .common import AplEntity, AplError, AplKeyError
|
|
||||||
from .wrapper import AplClient
|
|
||||||
|
|
||||||
apl_client = AplClient()
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"apl_client",
|
|
||||||
"AplClient",
|
|
||||||
"AplEntity",
|
|
||||||
"AplError",
|
|
||||||
"AplKeyError",
|
|
||||||
]
|
|
|
@ -1,6 +0,0 @@
|
||||||
from .base import AplBackend
|
|
||||||
from .file import FileAplBackend
|
|
||||||
from .mem import MemAplBackend
|
|
||||||
from .rest import RestAplBackend
|
|
||||||
|
|
||||||
__all__ = ["AplBackend", "MemAplBackend", "FileAplBackend", "RestAplBackend"]
|
|
|
@ -1,25 +0,0 @@
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from ..common import AplEntity
|
|
||||||
|
|
||||||
|
|
||||||
class AplBackend:
|
|
||||||
async def get(self, key: str) -> AplEntity:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"subclasses of BaseAPLClient must provide a get() method"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set(self, key: str, value: AplEntity):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"subclasses of BaseAPLClient must provide a add() method"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete(self, key: str):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"subclasses of BaseAPLClient must provide a delete() method"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_all(self, page_size: int) -> AsyncGenerator[tuple[str, AplEntity], None]:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"subclasses of BaseAPLClient must provide a delete() method"
|
|
||||||
)
|
|
|
@ -1,31 +0,0 @@
|
||||||
from json import dumps
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from pydantic import parse_file_as
|
|
||||||
|
|
||||||
from ..common import AplEntity
|
|
||||||
from .mem import MemAplBackend
|
|
||||||
|
|
||||||
|
|
||||||
class FileAplBackend(MemAplBackend):
|
|
||||||
def _load_file(self):
|
|
||||||
self._apl = parse_file_as(dict[str, AplEntity], self.path)
|
|
||||||
|
|
||||||
def _save_file(self):
|
|
||||||
data = dumps(jsonable_encoder(self._apl), indent=2)
|
|
||||||
self.path.write_text(data)
|
|
||||||
|
|
||||||
def __init__(self, path: Path):
|
|
||||||
super().__init__()
|
|
||||||
self.path = path
|
|
||||||
if self.path.exists():
|
|
||||||
self._load_file()
|
|
||||||
|
|
||||||
async def set(self, *args, **kwargs):
|
|
||||||
await super().set(*args, **kwargs)
|
|
||||||
self._save_file()
|
|
||||||
|
|
||||||
async def delete(self, *args, **kwargs):
|
|
||||||
await super().delete(*args, **kwargs)
|
|
||||||
self._save_file()
|
|
|
@ -1,26 +0,0 @@
|
||||||
from ..common import AplEntity, AplKeyError
|
|
||||||
from .base import AplBackend
|
|
||||||
|
|
||||||
|
|
||||||
class MemAplBackend(AplBackend):
|
|
||||||
def __init__(self):
|
|
||||||
self._apl: dict[str, AplEntity] = {}
|
|
||||||
|
|
||||||
async def get(self, key: str):
|
|
||||||
try:
|
|
||||||
return self._apl[key]
|
|
||||||
except KeyError as err:
|
|
||||||
raise AplKeyError(f"Key: {key} not found in MemApl") from err
|
|
||||||
|
|
||||||
async def set(self, key: str, value: AplEntity):
|
|
||||||
self._apl[key] = value
|
|
||||||
|
|
||||||
async def delete(self, key: str):
|
|
||||||
try:
|
|
||||||
del self._apl[key]
|
|
||||||
except KeyError as err:
|
|
||||||
raise AplKeyError(f"Key: {key} not found in MemApl") from err
|
|
||||||
|
|
||||||
async def get_all(self, page_size: int):
|
|
||||||
for key, val in self._apl.items():
|
|
||||||
yield key, val
|
|
|
@ -1,83 +0,0 @@
|
||||||
import json
|
|
||||||
from base64 import urlsafe_b64encode
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ..common import AplEntity, AplError, AplKeyError
|
|
||||||
from .base import AplBackend
|
|
||||||
|
|
||||||
|
|
||||||
class AplPage(BaseModel):
|
|
||||||
count: int
|
|
||||||
next: str | None
|
|
||||||
previous: str | None
|
|
||||||
results: list[AplEntity]
|
|
||||||
|
|
||||||
|
|
||||||
class RestAplBackend(AplBackend):
|
|
||||||
def __init__(self, apl_url: str, token: str):
|
|
||||||
self.apl_url = apl_url
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
@property
|
|
||||||
def headers(self):
|
|
||||||
return {"Authorization": f"Bearer {self.token}"}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def map_apl_entity(value: AplEntity):
|
|
||||||
return {
|
|
||||||
"saleor_app_id": value.app_id,
|
|
||||||
"saleor_api_url": value.saleor_api_url,
|
|
||||||
"jwks": json.dumps(value.jwks),
|
|
||||||
"domain": urlparse(value.saleor_api_url).netloc,
|
|
||||||
"token": value.app_token,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def b64_encode(key: str):
|
|
||||||
return urlsafe_b64encode(key.encode()).decode()
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def _client(self):
|
|
||||||
headers = {"Authorization": f"Bearer {self.token}"}
|
|
||||||
async with httpx.AsyncClient(base_url=self.apl_url, headers=headers) as client:
|
|
||||||
try:
|
|
||||||
yield client
|
|
||||||
except httpx.HTTPError as exc:
|
|
||||||
raise AplError("RestApl error") from exc
|
|
||||||
|
|
||||||
async def set(self, key: str, value: AplEntity):
|
|
||||||
async with self._client() as client:
|
|
||||||
resp = await client.post("/api/v1/apl", json=self.map_apl_entity(value))
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
async def get(self, key: str):
|
|
||||||
async with self._client() as client:
|
|
||||||
resp = await client.get(f"/api/v1/apl/{self.b64_encode(key)}")
|
|
||||||
if resp.status_code == 404:
|
|
||||||
raise AplKeyError(f"Key: {key} not found in RestApl")
|
|
||||||
resp.raise_for_status()
|
|
||||||
return AplEntity.parse_raw(resp.content)
|
|
||||||
|
|
||||||
async def delete(self, key: str):
|
|
||||||
async with self._client() as client:
|
|
||||||
resp = await client.delete(f"/api/v1/apl/{self.b64_encode(key)}")
|
|
||||||
if resp.status_code == 404:
|
|
||||||
raise AplKeyError(f"Key: {key} not found in RestApl")
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
async def get_all(self, page_size: int):
|
|
||||||
async with self._client() as client:
|
|
||||||
offset, count = 0, 1
|
|
||||||
while offset < count:
|
|
||||||
params = {"limit": page_size, "offset": offset}
|
|
||||||
resp = await client.get("/api/v1/apl", params=params)
|
|
||||||
resp.raise_for_status()
|
|
||||||
page = AplPage.parse_raw(resp.content)
|
|
||||||
count = page.count
|
|
||||||
offset += page_size
|
|
||||||
for elem in page.results:
|
|
||||||
yield elem.saleor_api_url, elem
|
|
|
@ -1,32 +0,0 @@
|
||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pydantic import AnyHttpUrl, BaseModel, Field, validator
|
|
||||||
|
|
||||||
|
|
||||||
class AplEntity(BaseModel):
|
|
||||||
saleor_api_url: AnyHttpUrl
|
|
||||||
app_id: str = Field(..., alias="saleor_app_id")
|
|
||||||
app_token: str = Field(..., alias="token")
|
|
||||||
jwks: dict[str, Any]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
@validator("jwks", pre=True)
|
|
||||||
def parse_json(cls, v):
|
|
||||||
if isinstance(v, str):
|
|
||||||
return json.loads(v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class AplError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AplKeyError(AplError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotConfiguredError(AplError):
|
|
||||||
"""If apl client was not configured"""
|
|
|
@ -1,135 +0,0 @@
|
||||||
interactions:
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
|
|
||||||
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:51:49 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: DELETE
|
|
||||||
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
|
|
||||||
response:
|
|
||||||
content: ''
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '0'
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:51:49 GMT
|
|
||||||
allow:
|
|
||||||
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 204
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
|
|
||||||
response:
|
|
||||||
content: '{"detail":"Not found."}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '23'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:51:50 GMT
|
|
||||||
allow:
|
|
||||||
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 404
|
|
||||||
version: 1
|
|
|
@ -1,44 +0,0 @@
|
||||||
interactions:
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: DELETE
|
|
||||||
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9ub24uZXhpc3RpbmcuZG9tYWlu
|
|
||||||
response:
|
|
||||||
content: '{"detail":"Not found."}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '23'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:59:52 GMT
|
|
||||||
allow:
|
|
||||||
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 404
|
|
||||||
version: 1
|
|
|
@ -1,98 +0,0 @@
|
||||||
interactions:
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
|
|
||||||
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:48:47 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:48:48 GMT
|
|
||||||
allow:
|
|
||||||
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
version: 1
|
|
|
@ -1,742 +0,0 @@
|
||||||
interactions:
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
|
|
||||||
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:37 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "fzglnnohvi", "saleor_api_url": "https://msltedyydo.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "msltedyydo.saleor.cloud",
|
|
||||||
"token": "etqfdiojfvufyfpylknharwbkkhcio"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://msltedyydo.saleor.cloud/graphql","token":"etqfdiojfvufyfpylknharwbkkhcio","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fzglnnohvi","domain":"msltedyydo.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:38 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "lcedtozpeo", "saleor_api_url": "https://rodnbaorcb.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "rodnbaorcb.saleor.cloud",
|
|
||||||
"token": "nyqokxizwonkuqqexhktbvgussupaz"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://rodnbaorcb.saleor.cloud/graphql","token":"nyqokxizwonkuqqexhktbvgussupaz","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"lcedtozpeo","domain":"rodnbaorcb.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:38 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "vxrgitlduz", "saleor_api_url": "https://vjoxywqxno.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vjoxywqxno.saleor.cloud",
|
|
||||||
"token": "uagntnmzugcynzcblilonyguxjowjq"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://vjoxywqxno.saleor.cloud/graphql","token":"uagntnmzugcynzcblilonyguxjowjq","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"vxrgitlduz","domain":"vjoxywqxno.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:39 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "mtljtfhfjd", "saleor_api_url": "https://txjaskukcx.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "txjaskukcx.saleor.cloud",
|
|
||||||
"token": "rzbhsxcngssozsamgksgqzsyykbvws"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://txjaskukcx.saleor.cloud/graphql","token":"rzbhsxcngssozsamgksgqzsyykbvws","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"mtljtfhfjd","domain":"txjaskukcx.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:39 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "krqomgdnue", "saleor_api_url": "https://wptoxotfln.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "wptoxotfln.saleor.cloud",
|
|
||||||
"token": "bfuugsnzlrmbtbfjhbsfduslfjbmty"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://wptoxotfln.saleor.cloud/graphql","token":"bfuugsnzlrmbtbfjhbsfduslfjbmty","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"krqomgdnue","domain":"wptoxotfln.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:39 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "qtpthrjavf", "saleor_api_url": "https://nxujxjrudl.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "nxujxjrudl.saleor.cloud",
|
|
||||||
"token": "vmwhdquypdsmsgjlcgtrqacmzbaunk"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://nxujxjrudl.saleor.cloud/graphql","token":"vmwhdquypdsmsgjlcgtrqacmzbaunk","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"qtpthrjavf","domain":"nxujxjrudl.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:40 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "nxvftnbrfb", "saleor_api_url": "https://vyyfdwmcav.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vyyfdwmcav.saleor.cloud",
|
|
||||||
"token": "wmsookfbmuopwwlvceiifcoaeicqje"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://vyyfdwmcav.saleor.cloud/graphql","token":"wmsookfbmuopwwlvceiifcoaeicqje","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"nxvftnbrfb","domain":"vyyfdwmcav.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:40 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "dpjoofueam", "saleor_api_url": "https://ejwnealqvw.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "ejwnealqvw.saleor.cloud",
|
|
||||||
"token": "teovwyfkqvpebpsmfpwigpqlusyrme"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://ejwnealqvw.saleor.cloud/graphql","token":"teovwyfkqvpebpsmfpwigpqlusyrme","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"dpjoofueam","domain":"ejwnealqvw.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:41 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: '{"saleor_app_id": "fxeyumrwrg", "saleor_api_url": "https://vxcdclzonj.saleor.cloud/graphql",
|
|
||||||
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vxcdclzonj.saleor.cloud",
|
|
||||||
"token": "vxhxemgqtpvvmzgcrxxetsrecvmocg"}'
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
content-length:
|
|
||||||
- '337'
|
|
||||||
content-type:
|
|
||||||
- application/json
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: POST
|
|
||||||
uri: https://apl.example.com/api/v1/apl
|
|
||||||
response:
|
|
||||||
content: '{"saleor_api_url":"https://vxcdclzonj.saleor.cloud/graphql","token":"vxhxemgqtpvvmzgcrxxetsrecvmocg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fxeyumrwrg","domain":"vxcdclzonj.saleor.cloud"}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '328'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:41 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 201
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl?limit=2&offset=0
|
|
||||||
response:
|
|
||||||
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=2","previous":null,"results":[{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"},{"saleor_api_url":"https://msltedyydo.saleor.cloud/graphql","token":"etqfdiojfvufyfpylknharwbkkhcio","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fzglnnohvi","domain":"msltedyydo.saleor.cloud"}]}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '779'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:41 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl?limit=2&offset=2
|
|
||||||
response:
|
|
||||||
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=4","previous":"http://apl.example.com/api/v1/apl?limit=2","results":[{"saleor_api_url":"https://rodnbaorcb.saleor.cloud/graphql","token":"nyqokxizwonkuqqexhktbvgussupaz","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"lcedtozpeo","domain":"rodnbaorcb.saleor.cloud"},{"saleor_api_url":"https://vjoxywqxno.saleor.cloud/graphql","token":"uagntnmzugcynzcblilonyguxjowjq","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"vxrgitlduz","domain":"vjoxywqxno.saleor.cloud"}]}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '839'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:42 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl?limit=2&offset=4
|
|
||||||
response:
|
|
||||||
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=6","previous":"http://apl.example.com/api/v1/apl?limit=2&offset=2","results":[{"saleor_api_url":"https://txjaskukcx.saleor.cloud/graphql","token":"rzbhsxcngssozsamgksgqzsyykbvws","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"mtljtfhfjd","domain":"txjaskukcx.saleor.cloud"},{"saleor_api_url":"https://wptoxotfln.saleor.cloud/graphql","token":"bfuugsnzlrmbtbfjhbsfduslfjbmty","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"krqomgdnue","domain":"wptoxotfln.saleor.cloud"}]}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '848'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:42 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl?limit=2&offset=6
|
|
||||||
response:
|
|
||||||
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=8","previous":"http://apl.example.com/api/v1/apl?limit=2&offset=4","results":[{"saleor_api_url":"https://nxujxjrudl.saleor.cloud/graphql","token":"vmwhdquypdsmsgjlcgtrqacmzbaunk","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"qtpthrjavf","domain":"nxujxjrudl.saleor.cloud"},{"saleor_api_url":"https://vyyfdwmcav.saleor.cloud/graphql","token":"wmsookfbmuopwwlvceiifcoaeicqje","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"nxvftnbrfb","domain":"vyyfdwmcav.saleor.cloud"}]}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '848'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:42 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl?limit=2&offset=8
|
|
||||||
response:
|
|
||||||
content: '{"count":10,"next":null,"previous":"http://apl.example.com/api/v1/apl?limit=2&offset=6","results":[{"saleor_api_url":"https://ejwnealqvw.saleor.cloud/graphql","token":"teovwyfkqvpebpsmfpwigpqlusyrme","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"dpjoofueam","domain":"ejwnealqvw.saleor.cloud"},{"saleor_api_url":"https://vxcdclzonj.saleor.cloud/graphql","token":"vxhxemgqtpvvmzgcrxxetsrecvmocg","jwks":"{\"keys\":
|
|
||||||
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
|
|
||||||
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fxeyumrwrg","domain":"vxcdclzonj.saleor.cloud"}]}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '779'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 17:07:43 GMT
|
|
||||||
allow:
|
|
||||||
- GET, POST, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 200
|
|
||||||
version: 1
|
|
|
@ -1,44 +0,0 @@
|
||||||
interactions:
|
|
||||||
- request:
|
|
||||||
body: ''
|
|
||||||
headers:
|
|
||||||
accept:
|
|
||||||
- '*/*'
|
|
||||||
accept-encoding:
|
|
||||||
- gzip, deflate
|
|
||||||
authorization:
|
|
||||||
- Bearer access.token
|
|
||||||
connection:
|
|
||||||
- keep-alive
|
|
||||||
host:
|
|
||||||
- apl.example.com
|
|
||||||
user-agent:
|
|
||||||
- python-httpx/0.23.3
|
|
||||||
method: GET
|
|
||||||
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9ub24uZXhpc3RpbmcuZG9tYWlu
|
|
||||||
response:
|
|
||||||
content: '{"detail":"Not found."}'
|
|
||||||
headers:
|
|
||||||
Connection:
|
|
||||||
- keep-alive
|
|
||||||
Content-Length:
|
|
||||||
- '23'
|
|
||||||
Content-Type:
|
|
||||||
- application/json
|
|
||||||
Date:
|
|
||||||
- Sun, 19 Feb 2023 16:59:27 GMT
|
|
||||||
allow:
|
|
||||||
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
||||||
referrer-policy:
|
|
||||||
- same-origin
|
|
||||||
server:
|
|
||||||
- uvicorn
|
|
||||||
vary:
|
|
||||||
- Origin
|
|
||||||
x-content-type-options:
|
|
||||||
- nosniff
|
|
||||||
x-frame-options:
|
|
||||||
- DENY
|
|
||||||
http_version: HTTP/1.1
|
|
||||||
status_code: 404
|
|
||||||
version: 1
|
|
|
@ -1,72 +0,0 @@
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..common import AplEntity
|
|
||||||
|
|
||||||
_SEED = 10
|
|
||||||
|
|
||||||
|
|
||||||
def _random_str(size: int, uppercase=False) -> str:
|
|
||||||
letters = string.ascii_uppercase if uppercase else string.ascii_lowercase
|
|
||||||
return "".join(random.choice(letters) for _ in range(size))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def anyio_backend():
|
|
||||||
return "asyncio", {"use_uvloop": True}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def vcr_config():
|
|
||||||
return {
|
|
||||||
"match_on": [
|
|
||||||
"method",
|
|
||||||
"scheme",
|
|
||||||
"host",
|
|
||||||
"port",
|
|
||||||
"path",
|
|
||||||
"query",
|
|
||||||
"headers",
|
|
||||||
"body",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
jwks_template = """
|
|
||||||
{{
|
|
||||||
"keys": [
|
|
||||||
{{
|
|
||||||
"kty": "RSA",
|
|
||||||
"key_ops": ["verify"],
|
|
||||||
"n": "{key_n}",
|
|
||||||
"e": "AQAB",
|
|
||||||
"use": "sig",
|
|
||||||
"kid": "1"
|
|
||||||
}}
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def entities_factory():
|
|
||||||
def factory(size=10):
|
|
||||||
random.seed(_SEED)
|
|
||||||
return [
|
|
||||||
AplEntity(
|
|
||||||
saleor_api_url=f"https://{_random_str(10)}.saleor.cloud/graphql",
|
|
||||||
app_id=_random_str(10),
|
|
||||||
app_token=_random_str(30),
|
|
||||||
jwks=jwks_template.format(key_n=_random_str(30)),
|
|
||||||
)
|
|
||||||
for _ in range(size)
|
|
||||||
]
|
|
||||||
|
|
||||||
return factory
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def entity(entities_factory) -> AplEntity:
|
|
||||||
return entities_factory()[0]
|
|
|
@ -1,134 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..backends import FileAplBackend, MemAplBackend, RestAplBackend
|
|
||||||
from ..common import AplKeyError, NotConfiguredError
|
|
||||||
from ..wrapper import AplClient
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mem_apl_client():
|
|
||||||
client = AplClient()
|
|
||||||
client.setup("mem://")
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def file_apl_client():
|
|
||||||
path_string = ".test_fileApl.json"
|
|
||||||
apl_file = Path(path_string)
|
|
||||||
client = AplClient()
|
|
||||||
client.setup(f"file://{path_string}")
|
|
||||||
yield client
|
|
||||||
if apl_file.exists():
|
|
||||||
apl_file.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def rest_apl_client():
|
|
||||||
client = AplClient()
|
|
||||||
client.setup("https://access.token@apl.example.com")
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
apl_clients = ["mem_apl_client", "file_apl_client", "rest_apl_client"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_settings_url_mem_backend():
|
|
||||||
backend = AplClient.parse_settings_url("mem://")
|
|
||||||
assert isinstance(backend, MemAplBackend)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"url,path",
|
|
||||||
[
|
|
||||||
("file://.fileApl.json", ".fileApl.json"),
|
|
||||||
("file://path/to/fileApl.json", "path/to/fileApl.json"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_parse_settings_url_file_backend(url, path):
|
|
||||||
backend = AplClient.parse_settings_url(url)
|
|
||||||
assert isinstance(backend, FileAplBackend)
|
|
||||||
assert backend.path == Path(path)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"url,apl_url,token",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"https://86b0bb44ee7e488d9cc3949b78b0a3ac.6dnRhRlMhhtjHXwBh3f3lJkmz4opGX7EInHKvxlMyPq6T5Y7@apl.example.com",
|
|
||||||
"https://apl.example.com",
|
|
||||||
"86b0bb44ee7e488d9cc3949b78b0a3ac.6dnRhRlMhhtjHXwBh3f3lJkmz4opGX7EInHKvxlMyPq6T5Y7",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"http://access.token@localhost:8000",
|
|
||||||
"http://localhost:8000",
|
|
||||||
"access.token",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_parse_settings_url_rest_backend(url, apl_url, token):
|
|
||||||
backend = AplClient.parse_settings_url(url)
|
|
||||||
assert isinstance(backend, RestAplBackend)
|
|
||||||
assert backend.apl_url == apl_url
|
|
||||||
assert backend.token == token
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_settings_url_not_supported_backend():
|
|
||||||
with pytest.raises(NotImplementedError):
|
|
||||||
AplClient.parse_settings_url("new-schema://path")
|
|
||||||
|
|
||||||
|
|
||||||
async def test_apl_client_no_setup():
|
|
||||||
client = AplClient()
|
|
||||||
with pytest.raises(NotConfiguredError):
|
|
||||||
await client.get("shop.saleor.cloud")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
|
||||||
@pytest.mark.parametrize("client_fixture", apl_clients)
|
|
||||||
async def test_apl_get(client_fixture, entity, request):
|
|
||||||
apl_client: AplClient = request.getfixturevalue(client_fixture)
|
|
||||||
await apl_client.set(entity.saleor_api_url, entity)
|
|
||||||
entity_from_client = await apl_client.get(entity.saleor_api_url)
|
|
||||||
assert entity_from_client == entity.copy()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
|
||||||
@pytest.mark.parametrize("client_fixture", apl_clients)
|
|
||||||
async def test_apl_key_error(client_fixture, request):
|
|
||||||
apl_client: AplClient = request.getfixturevalue(client_fixture)
|
|
||||||
with pytest.raises(AplKeyError):
|
|
||||||
await apl_client.get("https://non.existing.domain")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
|
||||||
@pytest.mark.parametrize("client_fixture", apl_clients)
|
|
||||||
async def test_apl_delete(client_fixture, entity, request):
|
|
||||||
apl_client: AplClient = request.getfixturevalue(client_fixture)
|
|
||||||
await apl_client.set(entity.saleor_api_url, entity)
|
|
||||||
await apl_client.delete(entity.saleor_api_url)
|
|
||||||
with pytest.raises(AplKeyError):
|
|
||||||
await apl_client.get(entity.saleor_api_url)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
|
||||||
@pytest.mark.parametrize("client_fixture", apl_clients)
|
|
||||||
async def test_apl_delete_non_existing(client_fixture, request):
|
|
||||||
apl_client: AplClient = request.getfixturevalue(client_fixture)
|
|
||||||
with pytest.raises(AplKeyError):
|
|
||||||
await apl_client.delete("https://non.existing.domain")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
|
||||||
@pytest.mark.parametrize("client_fixture", apl_clients)
|
|
||||||
async def test_apl_get_all(client_fixture, entities_factory, request):
|
|
||||||
apl_client: AplClient = request.getfixturevalue(client_fixture)
|
|
||||||
entities = entities_factory()
|
|
||||||
for entity in entities:
|
|
||||||
await apl_client.set(entity.saleor_api_url, entity)
|
|
||||||
apl_entities = [entity async for entity in apl_client.get_all(page_size=2)]
|
|
||||||
assert apl_entities == [(entity.saleor_api_url, entity) for entity in entities]
|
|
|
@ -1,49 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from .backends import AplBackend, FileAplBackend, MemAplBackend, RestAplBackend
|
|
||||||
from .common import AplEntity, NotConfiguredError
|
|
||||||
|
|
||||||
|
|
||||||
class AplClient:
|
|
||||||
def __init__(self):
|
|
||||||
self._backend: AplBackend | None = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_settings_url(url: str) -> AplBackend:
|
|
||||||
parts = urlparse(url)
|
|
||||||
if parts.scheme == "file":
|
|
||||||
path = Path(url.removeprefix("file://"))
|
|
||||||
return FileAplBackend(path)
|
|
||||||
elif parts.scheme == "mem":
|
|
||||||
return MemAplBackend()
|
|
||||||
elif parts.scheme in ["http", "https"]:
|
|
||||||
token = parts.username or ""
|
|
||||||
port = "" if parts.port is None else f":{parts.port}"
|
|
||||||
apl_url = f"{parts.scheme}://{parts.hostname}{port}{parts.path}"
|
|
||||||
return RestAplBackend(apl_url=apl_url, token=token)
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def setup(self, url: str):
|
|
||||||
self._backend = self.parse_settings_url(url)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def backend(self) -> AplBackend:
|
|
||||||
if self._backend is None:
|
|
||||||
raise NotConfiguredError(
|
|
||||||
"Run `apl_client.setup(...)` before using apl_client"
|
|
||||||
)
|
|
||||||
return self._backend
|
|
||||||
|
|
||||||
async def get(self, key: str) -> AplEntity:
|
|
||||||
return await self.backend.get(key)
|
|
||||||
|
|
||||||
async def set(self, key: str, value: AplEntity):
|
|
||||||
return await self.backend.set(key, value)
|
|
||||||
|
|
||||||
async def delete(self, key: str):
|
|
||||||
return await self.backend.delete(key)
|
|
||||||
|
|
||||||
async def get_all(self, page_size=100):
|
|
||||||
async for item in self.backend.get_all(page_size=page_size):
|
|
||||||
yield item
|
|
|
@ -1,131 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request, status
|
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
from .api import graphql_app
|
|
||||||
from .apl import AplEntity, apl_client
|
|
||||||
from .deps import (
|
|
||||||
ApiDependencies,
|
|
||||||
WebhookDependencies,
|
|
||||||
saleor_api_url_header,
|
|
||||||
verify_saleor_api_url,
|
|
||||||
)
|
|
||||||
from .integrations.datadog import DataDogClientError
|
|
||||||
from .logs import configure_logging
|
|
||||||
from .payload import OBSERVABILITY_EVENTS
|
|
||||||
from .saleor.client import GraphQLError, SaleorClient
|
|
||||||
from .saleor.common import InstallData, LazyUrl
|
|
||||||
from .settings import manifest, settings
|
|
||||||
from .utils import get_base_url
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
configure_logging(settings.debug)
|
|
||||||
|
|
||||||
apl_client.setup(settings.apl_url)
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(openapi_url="/api/openapi.json")
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
allow_methods=["OPTIONS", "GET", "POST", "DELETE"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/manifest", status_code=status.HTTP_200_OK, tags=["installation-handshake"])
|
|
||||||
async def show_manifest(request: Request):
|
|
||||||
manifest_copy = manifest.copy()
|
|
||||||
for name, field in manifest_copy:
|
|
||||||
if isinstance(field, LazyUrl):
|
|
||||||
setattr(manifest_copy, name, field(request))
|
|
||||||
for extension in manifest_copy.extensions:
|
|
||||||
if isinstance(extension.url, LazyUrl):
|
|
||||||
extension.url = extension.url(request)
|
|
||||||
for webhook in manifest_copy.webhooks:
|
|
||||||
if isinstance(webhook.target_url, LazyUrl):
|
|
||||||
webhook.target_url = webhook.target_url(request)
|
|
||||||
return manifest_copy
|
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
|
||||||
"/install",
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
tags=["installation-handshake"],
|
|
||||||
name="install",
|
|
||||||
)
|
|
||||||
async def install(
|
|
||||||
data: InstallData,
|
|
||||||
request: Request,
|
|
||||||
api_url=Depends(saleor_api_url_header),
|
|
||||||
_valid=Depends(verify_saleor_api_url),
|
|
||||||
):
|
|
||||||
client = SaleorClient(api_url, "test", data.auth_token)
|
|
||||||
try:
|
|
||||||
app_info = await client.app_info()
|
|
||||||
target_url = get_base_url(request).replace(path="webhooks")
|
|
||||||
await client.create_webhook(
|
|
||||||
target_url=str(target_url),
|
|
||||||
events=["OBSERVABILITY"],
|
|
||||||
name="OBSERVABILITY",
|
|
||||||
)
|
|
||||||
jwks = await client.get_jwks()
|
|
||||||
entity = AplEntity(
|
|
||||||
saleor_api_url=api_url,
|
|
||||||
app_id=app_info.id,
|
|
||||||
app_token=data.auth_token,
|
|
||||||
jwks=jwks,
|
|
||||||
)
|
|
||||||
await apl_client.set(api_url, entity)
|
|
||||||
except GraphQLError:
|
|
||||||
return {"error": {"message": "Wrong app token"}}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
|
||||||
"/webhooks",
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
tags=["webhooks"],
|
|
||||||
name="webhooks",
|
|
||||||
)
|
|
||||||
async def handle_observability_events(
|
|
||||||
payloads: list[OBSERVABILITY_EVENTS],
|
|
||||||
commons: WebhookDependencies = Depends(),
|
|
||||||
):
|
|
||||||
metadata = await commons.manager.get_metadata()
|
|
||||||
if not metadata.datadog or not metadata.datadog.active:
|
|
||||||
logger.warning(
|
|
||||||
"DataDog integration inactive or not configured. Dropping %s events",
|
|
||||||
len(payloads),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
credentials = metadata.datadog.credentials
|
|
||||||
try:
|
|
||||||
logger.info("Sending %s events to DataDog", len(payloads))
|
|
||||||
await commons.datadog_client.send_logs(
|
|
||||||
commons.saleor_data.saleor_api_url, credentials, payloads
|
|
||||||
)
|
|
||||||
except DataDogClientError:
|
|
||||||
logger.warning("Sending logs to DataDog failed. Deactivating integration")
|
|
||||||
metadata.datadog.active = False
|
|
||||||
metadata.datadog.error = "Wrong credentials. Integration deactivated"
|
|
||||||
await commons.manager.save_private_metadata(metadata)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health", status_code=status.HTTP_200_OK, tags=["health"], name="health")
|
|
||||||
async def health():
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/graphql")
|
|
||||||
async def graphiql(request: Request):
|
|
||||||
return await graphql_app.render_playground(request)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/graphql")
|
|
||||||
async def graphql(request: Request, commons: ApiDependencies = Depends()):
|
|
||||||
request.state.api_context = commons
|
|
||||||
return await graphql_app.graphql_http_server(request)
|
|
|
@ -1,143 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import Depends, Header, HTTPException, Request
|
|
||||||
from jwt.api_jwk import PyJWKSet
|
|
||||||
|
|
||||||
from .apl import AplEntity, AplError, AplKeyError, apl_client
|
|
||||||
from .integrations.datadog import DataDogApiClient, DataDogClient, TestDataDogClient
|
|
||||||
from .saleor import (
|
|
||||||
SALEOR_API_URL_HEADER,
|
|
||||||
SALEOR_SIGNATURE_HEADER,
|
|
||||||
SALEOR_TOKEN_HEADER,
|
|
||||||
SaleorToken,
|
|
||||||
)
|
|
||||||
from .saleor.crypto import decode_webhook_payload
|
|
||||||
from .saleor.manager import SaleorManager
|
|
||||||
from .schema import Metadata
|
|
||||||
from .settings import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def saleor_api_url_header(
|
|
||||||
api_url: str | None = Header(None, alias=SALEOR_API_URL_HEADER)
|
|
||||||
) -> str:
|
|
||||||
if not api_url:
|
|
||||||
msg = f"Missing {SALEOR_API_URL_HEADER.upper()} header."
|
|
||||||
logger.warning(msg)
|
|
||||||
raise HTTPException(status_code=400, detail=msg)
|
|
||||||
return api_url
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_saleor_api_url(api_url: str = Depends(saleor_api_url_header)):
|
|
||||||
# TODO implement
|
|
||||||
if api_url:
|
|
||||||
return True
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f"Provided url is invalid or environment {api_url} not allowed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def saleor_token_header(
|
|
||||||
token: str | None = Header(None, alias=SALEOR_TOKEN_HEADER)
|
|
||||||
) -> str:
|
|
||||||
if not token:
|
|
||||||
msg = f"Missing {SALEOR_TOKEN_HEADER.upper()} header."
|
|
||||||
logger.warning(msg)
|
|
||||||
raise HTTPException(status_code=400, detail=msg)
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
async def apl_entity(
|
|
||||||
api_url: str = Depends(saleor_api_url_header),
|
|
||||||
_verify=Depends(verify_saleor_api_url),
|
|
||||||
) -> AplEntity:
|
|
||||||
try:
|
|
||||||
return await apl_client.get(api_url)
|
|
||||||
except AplKeyError:
|
|
||||||
msg = f"App needs to be installed before use. First, install the app in Saleor at {api_url}."
|
|
||||||
logger.warning(msg)
|
|
||||||
raise HTTPException(status_code=400, detail=msg) # noqa: 904
|
|
||||||
except AplError as error:
|
|
||||||
logging.error("APL error %r", error)
|
|
||||||
raise HTTPException( # noqa: 904
|
|
||||||
status_code=500, detail="App error. Try again later."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def saleor_token(
|
|
||||||
token_str: str = Depends(saleor_token_header),
|
|
||||||
saleor_data: AplEntity = Depends(apl_entity),
|
|
||||||
) -> SaleorToken:
|
|
||||||
token = SaleorToken(token_str, saleor_data.jwks)
|
|
||||||
# TODO handle jwks reload
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_is_stuff_user(token: SaleorToken = Depends(saleor_token)) -> bool:
|
|
||||||
if not token.is_staff_user():
|
|
||||||
HTTPException(
|
|
||||||
status_code=400, detail="Only staff user can perform this operation."
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def get_saleor_signature(
|
|
||||||
saleor_signature: str | None = Header(None, alias=SALEOR_SIGNATURE_HEADER)
|
|
||||||
):
|
|
||||||
return saleor_signature
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_webhook_signature(
|
|
||||||
request: Request,
|
|
||||||
jws: str = Depends(get_saleor_signature),
|
|
||||||
saleor_data: AplEntity = Depends(apl_entity),
|
|
||||||
):
|
|
||||||
saleor_jwks = PyJWKSet.from_dict(saleor_data.jwks)
|
|
||||||
return decode_webhook_payload(
|
|
||||||
jws=jws, jwks=saleor_jwks, webhook_payload=await request.body()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def saleor_manager(saleor_data: AplEntity = Depends(apl_entity)):
|
|
||||||
return SaleorManager[Metadata](
|
|
||||||
saleor_data.saleor_api_url,
|
|
||||||
saleor_data.app_id,
|
|
||||||
saleor_data.app_token,
|
|
||||||
Metadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def datadog_client() -> DataDogClient:
|
|
||||||
if settings.mock_datadog_client:
|
|
||||||
return TestDataDogClient()
|
|
||||||
return DataDogApiClient()
|
|
||||||
|
|
||||||
|
|
||||||
class ApiDependencies:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
token: SaleorToken = Depends(saleor_token),
|
|
||||||
saleor_data: AplEntity = Depends(apl_entity),
|
|
||||||
manager: SaleorManager[Metadata] = Depends(saleor_manager),
|
|
||||||
datadog_client: DataDogClient = Depends(datadog_client),
|
|
||||||
_verify_is_stuff_user=Depends(verify_is_stuff_user),
|
|
||||||
):
|
|
||||||
self.saleor_data = saleor_data
|
|
||||||
self.token = token
|
|
||||||
self.manager = manager
|
|
||||||
self.datadog_client = datadog_client
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookDependencies:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
saleor_data: AplEntity = Depends(apl_entity),
|
|
||||||
manager: SaleorManager[Metadata] = Depends(saleor_manager),
|
|
||||||
datadog_client: DataDogClient = Depends(datadog_client),
|
|
||||||
_verify_webhook_signature=Depends(verify_webhook_signature),
|
|
||||||
):
|
|
||||||
self.saleor_data = saleor_data
|
|
||||||
self.manager = manager
|
|
||||||
self.datadog_client = datadog_client
|
|
|
@ -1,145 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import Any, Awaitable, cast
|
|
||||||
|
|
||||||
from datadog_api_client.exceptions import ApiException, ForbiddenException
|
|
||||||
from datadog_api_client.v1.api.authentication_api import AuthenticationApi
|
|
||||||
from datadog_api_client.v2 import AsyncApiClient, Configuration
|
|
||||||
from datadog_api_client.v2.api.logs_api import LogsApi
|
|
||||||
from datadog_api_client.v2.model.content_encoding import ContentEncoding
|
|
||||||
from datadog_api_client.v2.model.http_log import HTTPLog
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ..payload import OBSERVABILITY_EVENTS
|
|
||||||
from .logs import generate_logs
|
|
||||||
|
|
||||||
LOGS_ENCODING = ContentEncoding("gzip")
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
LOGS_ERRORS = {
|
|
||||||
400: "Bad request (likely an issue in the payload formatting)",
|
|
||||||
401: "Unauthorized (likely a missing API Key)",
|
|
||||||
403: "Permission issue (likely using an invalid API Key)",
|
|
||||||
408: "Request Timeout, request should be retried after some time",
|
|
||||||
413: "Payload too large (batch is above 5MB uncompressed)",
|
|
||||||
429: "Too Many Requests, request should be retried after some time",
|
|
||||||
500: "Internal Server Error, the server encountered an unexpected condition that "
|
|
||||||
"prevented it from fulfilling the request, request should be retried after some time",
|
|
||||||
503: "Service Unavailable, the server is not ready to handle the request "
|
|
||||||
"probably because it is overloaded, request should be retried after some time",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogClientError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogCredsRejectedError(DataDogClientError):
|
|
||||||
"""DataDog credentials were rejected"""
|
|
||||||
|
|
||||||
|
|
||||||
class DatadogCredentials(BaseModel):
|
|
||||||
site: str
|
|
||||||
api_key: str
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogClient:
|
|
||||||
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def send_logs(
|
|
||||||
self,
|
|
||||||
saleor_api_url: str,
|
|
||||||
credentials: DatadogCredentials,
|
|
||||||
logs: list[OBSERVABILITY_EVENTS],
|
|
||||||
):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataDogClient(DataDogClient):
|
|
||||||
_good_creds = DatadogCredentials(
|
|
||||||
site="US1", api_key="156e22d50c4e8b6816e1fd4794d3fd8c"
|
|
||||||
)
|
|
||||||
_failing_creds = DatadogCredentials(
|
|
||||||
site="EU1", api_key="156e22d50c4e8b6816e1fd4794d3fd8c"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
|
|
||||||
return credentials == self._good_creds or credentials == self._failing_creds
|
|
||||||
|
|
||||||
async def send_logs(
|
|
||||||
self,
|
|
||||||
saleor_api_url: str,
|
|
||||||
credentials: DatadogCredentials,
|
|
||||||
logs: list[OBSERVABILITY_EVENTS],
|
|
||||||
):
|
|
||||||
if credentials == self._good_creds:
|
|
||||||
return
|
|
||||||
raise DataDogCredsRejectedError()
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogApiClient(DataDogClient):
|
|
||||||
_site_map = {
|
|
||||||
"US1": "datadoghq.com",
|
|
||||||
"US3": "us3.datadoghq.com",
|
|
||||||
"US5": "us5.datadoghq.com",
|
|
||||||
"EU1": "datadoghq.eu",
|
|
||||||
"US1_FED": "ddog-gov.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_config(cls, credentials: DatadogCredentials) -> Configuration:
|
|
||||||
configuration = Configuration()
|
|
||||||
configuration.api_key["apiKeyAuth"] = credentials.api_key
|
|
||||||
configuration.server_variables["site"] = cls._site_map[credentials.site]
|
|
||||||
return configuration
|
|
||||||
|
|
||||||
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
|
|
||||||
config = self._get_config(credentials)
|
|
||||||
async with AsyncApiClient(config) as api_client:
|
|
||||||
api_instance = AuthenticationApi(api_client)
|
|
||||||
try:
|
|
||||||
response = await cast(Awaitable[Any], api_instance.validate())
|
|
||||||
return response.valid
|
|
||||||
except ForbiddenException:
|
|
||||||
pass
|
|
||||||
except ApiException as exp:
|
|
||||||
logger.error(
|
|
||||||
"DataDog validate_credentials ApiException[%s]",
|
|
||||||
exp.status,
|
|
||||||
extra={
|
|
||||||
"status_code": exp.status,
|
|
||||||
"response_headers": dict(exp.headers) if exp.headers else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def send_logs(
|
|
||||||
self,
|
|
||||||
saleor_api_url: str,
|
|
||||||
credentials: DatadogCredentials,
|
|
||||||
logs: list[OBSERVABILITY_EVENTS],
|
|
||||||
):
|
|
||||||
config = self._get_config(credentials)
|
|
||||||
logs = generate_logs(logs, saleor_api_url)
|
|
||||||
async with AsyncApiClient(config) as api_client:
|
|
||||||
api_instance = LogsApi(api_client)
|
|
||||||
try:
|
|
||||||
await cast(
|
|
||||||
Awaitable[Any],
|
|
||||||
api_instance.submit_log(
|
|
||||||
body=HTTPLog(logs), content_encoding=LOGS_ENCODING
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except ApiException as exp:
|
|
||||||
error_msg = LOGS_ERRORS.get(exp.status)
|
|
||||||
logger.error(
|
|
||||||
"DataDog send_logs ApiException[%s]",
|
|
||||||
error_msg,
|
|
||||||
extra={
|
|
||||||
"status_code": exp.status,
|
|
||||||
"response_headers": dict(exp.headers) if exp.headers else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.error("DataDog send_logs Unknown error")
|
|
|
@ -1,291 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from ua_parser import user_agent_parser # type: ignore
|
|
||||||
|
|
||||||
from ..payload import (
|
|
||||||
OBSERVABILITY_EVENTS,
|
|
||||||
ApiCallPayload,
|
|
||||||
EventDeliveryAttemptPayload,
|
|
||||||
JsonTruncText,
|
|
||||||
)
|
|
||||||
from ..utils import parse_headers
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryStatus(str, Enum):
|
|
||||||
pending = "pending"
|
|
||||||
success = "success"
|
|
||||||
failed = "failed"
|
|
||||||
|
|
||||||
|
|
||||||
class LogLevel(str, Enum):
|
|
||||||
info = "INFO"
|
|
||||||
warn = "WARN"
|
|
||||||
error = "ERROR"
|
|
||||||
|
|
||||||
|
|
||||||
log_level_map = {
|
|
||||||
EventDeliveryStatus.pending.value: LogLevel.warn,
|
|
||||||
EventDeliveryStatus.success.value: LogLevel.info,
|
|
||||||
EventDeliveryStatus.failed.value: LogLevel.error,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogLogUseragentDetails(BaseModel):
|
|
||||||
os: dict[str, str | None]
|
|
||||||
browser: dict[str, str | None]
|
|
||||||
device: dict[str, str | None]
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogLogNetwork(BaseModel):
|
|
||||||
bytes_read: int | None
|
|
||||||
bytes_written: int | None
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogLogEvent(BaseModel):
|
|
||||||
name: str
|
|
||||||
outcome: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogLogHttp(BaseModel):
|
|
||||||
url: str
|
|
||||||
request_id: str | None
|
|
||||||
referer: str | None
|
|
||||||
status_code: str | None
|
|
||||||
method: str | None
|
|
||||||
useragent: str | None
|
|
||||||
useragent_details: DataDogLogUseragentDetails | None
|
|
||||||
|
|
||||||
|
|
||||||
class DataDogLog(BaseModel):
|
|
||||||
ddsource: str = "saleor"
|
|
||||||
service: str
|
|
||||||
timestamp: float
|
|
||||||
level: LogLevel = LogLevel.info
|
|
||||||
ddtags: str | None
|
|
||||||
http: DataDogLogHttp
|
|
||||||
network: DataDogLogNetwork
|
|
||||||
evt: DataDogLogEvent
|
|
||||||
duration: int | None
|
|
||||||
|
|
||||||
|
|
||||||
class GraphQLOperation(BaseModel):
|
|
||||||
name: str | None
|
|
||||||
operation_type: str
|
|
||||||
query: str | None
|
|
||||||
result: str | None
|
|
||||||
result_invalid: bool
|
|
||||||
truncated: bool
|
|
||||||
|
|
||||||
|
|
||||||
class EventRequest(BaseModel):
|
|
||||||
headers: dict[str, str] = {}
|
|
||||||
|
|
||||||
|
|
||||||
class EventResponse(BaseModel):
|
|
||||||
headers: dict[str, str] = {}
|
|
||||||
body: JsonTruncText | None
|
|
||||||
|
|
||||||
|
|
||||||
class App(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCall(BaseModel):
|
|
||||||
request_headers: dict[str, str] = {}
|
|
||||||
response_headers: dict[str, str] = {}
|
|
||||||
gql_operations: list[GraphQLOperation]
|
|
||||||
app: App | None
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCallLog(DataDogLog):
|
|
||||||
saleor: ApiCall
|
|
||||||
|
|
||||||
|
|
||||||
class Webhook(BaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
target_url: str
|
|
||||||
subscription_query: JsonTruncText | None
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryPayload(BaseModel):
|
|
||||||
content_length: int
|
|
||||||
body: JsonTruncText
|
|
||||||
|
|
||||||
|
|
||||||
class EventDelivery(BaseModel):
|
|
||||||
id: str
|
|
||||||
status: str
|
|
||||||
event_type: str
|
|
||||||
event_sync: bool
|
|
||||||
payload: EventDeliveryPayload
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttempt(BaseModel):
|
|
||||||
id: str
|
|
||||||
status: str
|
|
||||||
next_retry: datetime | None
|
|
||||||
request_headers: dict[str, str] = {}
|
|
||||||
response_headers: dict[str, str] = {}
|
|
||||||
response_body: JsonTruncText | None
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttemptData(BaseModel):
|
|
||||||
event_delivery_attempt: EventDeliveryAttempt
|
|
||||||
event_delivery: EventDelivery
|
|
||||||
webhook: Webhook
|
|
||||||
app: App
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttemptLog(DataDogLog):
|
|
||||||
saleor: EventDeliveryAttemptData
|
|
||||||
|
|
||||||
|
|
||||||
def http_status_ok(status_code: int | None):
|
|
||||||
if status_code is None or status_code >= 400:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def parse_useragent(useragent: str | None) -> DataDogLogUseragentDetails | None:
|
|
||||||
if not useragent:
|
|
||||||
return None
|
|
||||||
parsed = user_agent_parser.Parse(useragent)
|
|
||||||
return DataDogLogUseragentDetails(
|
|
||||||
os=parsed["os"],
|
|
||||||
browser=parsed["user_agent"],
|
|
||||||
device=parsed["device"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_duration(duration: float | None) -> float | None:
|
|
||||||
return duration * 1_000_000_000 if duration else None
|
|
||||||
|
|
||||||
|
|
||||||
def generate_api_call_log(payload: ApiCallPayload, saleor_domain: str) -> HTTPLogItem:
|
|
||||||
request_headers = parse_headers(payload.request.headers)
|
|
||||||
response_headers = parse_headers(payload.response.headers)
|
|
||||||
gql_operations: list[GraphQLOperation] = []
|
|
||||||
for op in payload.gql_operations:
|
|
||||||
gql_operations.append(
|
|
||||||
GraphQLOperation(
|
|
||||||
name=op.name.text if op.name else None,
|
|
||||||
operation_type=op.operation_type,
|
|
||||||
query=op.query.text if op.query else None,
|
|
||||||
result=op.result.text if op.result else None,
|
|
||||||
result_invalid=op.result_invalid,
|
|
||||||
truncated=any(
|
|
||||||
[
|
|
||||||
op.name.truncated if op.name else False,
|
|
||||||
op.query.truncated if op.query else False,
|
|
||||||
op.result.truncated if op.result else False,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
level = (
|
|
||||||
LogLevel.info
|
|
||||||
if http_status_ok(payload.response.status_code)
|
|
||||||
else LogLevel.error
|
|
||||||
)
|
|
||||||
message = ApiCallLog(
|
|
||||||
service=saleor_domain,
|
|
||||||
timestamp=payload.request.time.timestamp() * 1000,
|
|
||||||
level=level,
|
|
||||||
http=DataDogLogHttp(
|
|
||||||
url=payload.request.url,
|
|
||||||
request_id=payload.request.id,
|
|
||||||
referer=request_headers.get("Referer"),
|
|
||||||
status_code=str(payload.response.status_code),
|
|
||||||
method=payload.request.method,
|
|
||||||
useragent=request_headers.get("User-Agent"),
|
|
||||||
useragent_details=parse_useragent(request_headers.get("User-Agent")),
|
|
||||||
),
|
|
||||||
network=DataDogLogNetwork(
|
|
||||||
bytes_read=payload.request.content_length,
|
|
||||||
bytes_written=payload.response.content_length,
|
|
||||||
),
|
|
||||||
evt=DataDogLogEvent(
|
|
||||||
name=payload.event_type,
|
|
||||||
outcome="success" if level == LogLevel.info else "failure",
|
|
||||||
),
|
|
||||||
saleor=ApiCall(
|
|
||||||
gql_operations=gql_operations,
|
|
||||||
app=App(id=payload.app.id, name=payload.app.name) if payload.app else None,
|
|
||||||
request_headers=dict(request_headers),
|
|
||||||
response_headers=dict(response_headers),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return HTTPLogItem(message=message.json())
|
|
||||||
|
|
||||||
|
|
||||||
def generate_event_delivery_attempt_log(
|
|
||||||
payload: EventDeliveryAttemptPayload, saleor_domain: str
|
|
||||||
) -> HTTPLogItem:
|
|
||||||
request_headers = parse_headers(payload.request.headers)
|
|
||||||
response_headers = parse_headers(payload.response.headers)
|
|
||||||
message = EventDeliveryAttemptLog(
|
|
||||||
service=saleor_domain,
|
|
||||||
timestamp=payload.time.timestamp() * 1000,
|
|
||||||
level=log_level_map.get(payload.event_delivery.status, LogLevel.error),
|
|
||||||
duration=convert_duration(payload.duration),
|
|
||||||
http=DataDogLogHttp(
|
|
||||||
url=payload.webhook.target_url,
|
|
||||||
request_id=payload.id,
|
|
||||||
status_code=str(payload.response.status_code),
|
|
||||||
),
|
|
||||||
network=DataDogLogNetwork(
|
|
||||||
bytes_read=payload.response.content_length,
|
|
||||||
bytes_written=payload.event_delivery.payload.content_length,
|
|
||||||
),
|
|
||||||
evt=DataDogLogEvent(
|
|
||||||
name=payload.event_type,
|
|
||||||
outcome=payload.status,
|
|
||||||
),
|
|
||||||
saleor=EventDeliveryAttemptData(
|
|
||||||
event_delivery_attempt=EventDeliveryAttempt(
|
|
||||||
id=payload.id,
|
|
||||||
status=payload.status,
|
|
||||||
next_retry=payload.next_retry,
|
|
||||||
request_headers=dict(request_headers),
|
|
||||||
response_headers=dict(response_headers),
|
|
||||||
response_body=payload.response.body,
|
|
||||||
),
|
|
||||||
app=App(id=payload.app.id, name=payload.app.name),
|
|
||||||
event_delivery=EventDelivery(
|
|
||||||
id=payload.event_delivery.id,
|
|
||||||
status=payload.event_delivery.status,
|
|
||||||
event_type=payload.event_delivery.event_type,
|
|
||||||
event_sync=payload.event_delivery.event_sync,
|
|
||||||
payload=EventDeliveryPayload(
|
|
||||||
body=payload.event_delivery.payload.body,
|
|
||||||
content_length=payload.event_delivery.payload.content_length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
webhook=Webhook(
|
|
||||||
id=payload.webhook.id,
|
|
||||||
name=payload.webhook.name,
|
|
||||||
target_url=payload.webhook.target_url,
|
|
||||||
subscription_query=payload.webhook.subscription_query,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return HTTPLogItem(message=message.json())
|
|
||||||
|
|
||||||
|
|
||||||
def generate_logs(
|
|
||||||
payloads: list[OBSERVABILITY_EVENTS], saleor_domain: str
|
|
||||||
) -> list[HTTPLogItem]:
|
|
||||||
log_items: list[HTTPLogItem] = []
|
|
||||||
for payload in payloads:
|
|
||||||
if isinstance(payload, ApiCallPayload):
|
|
||||||
log_items.append(generate_api_call_log(payload, saleor_domain))
|
|
||||||
elif isinstance(payload, EventDeliveryAttemptPayload):
|
|
||||||
log_items.append(
|
|
||||||
generate_event_delivery_attempt_log(payload, saleor_domain)
|
|
||||||
)
|
|
||||||
return log_items
|
|
|
@ -1,62 +0,0 @@
|
||||||
import logging.config
|
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(debug=False):
|
|
||||||
config = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"root": {
|
|
||||||
"level": "INFO",
|
|
||||||
"handlers": ["default"],
|
|
||||||
},
|
|
||||||
"formatters": {
|
|
||||||
"json": {
|
|
||||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
|
||||||
"datefmt": "%Y-%m-%dT%H:%M:%SZ",
|
|
||||||
"format": (
|
|
||||||
"%(asctime)s %(levelname)s %(lineno)s %(message)s %(name)s "
|
|
||||||
+ "%(pathname)s %(process)d %(threadName)s"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"verbose": {
|
|
||||||
"format": (
|
|
||||||
"%(levelname)s %(name)s %(message)s [PID:%(process)d:%(threadName)s]"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"uvicorn": {
|
|
||||||
"()": "uvicorn.logging.DefaultFormatter",
|
|
||||||
"fmt": "%(levelprefix)s %(message)s [PID:%(process)d:%(threadName)s]",
|
|
||||||
"use_colors": None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"default": {
|
|
||||||
"level": "DEBUG",
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"formatter": "verbose" if debug else "json",
|
|
||||||
},
|
|
||||||
"uvicorn": {
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"formatter": "uvicorn" if debug else "json",
|
|
||||||
},
|
|
||||||
"null": {
|
|
||||||
"class": "logging.NullHandler",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"loggers": {
|
|
||||||
"uvicorn": {
|
|
||||||
"propagate": False,
|
|
||||||
"handlers": ["uvicorn"],
|
|
||||||
"level": "INFO",
|
|
||||||
},
|
|
||||||
"uvicorn.access": {
|
|
||||||
"propagate": False,
|
|
||||||
"handlers": ["null"],
|
|
||||||
},
|
|
||||||
"saleor_app_observability": {
|
|
||||||
"level": "DEBUG" if debug else "INFO",
|
|
||||||
"propagate": True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
logging.config.dictConfig(config)
|
|
|
@ -1,93 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
from .utils import HttpHeaders, JsonBaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class App(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCallRequest(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
method: str
|
|
||||||
url: str
|
|
||||||
time: datetime
|
|
||||||
headers: HttpHeaders
|
|
||||||
content_length: int
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCallResponse(JsonBaseModel):
|
|
||||||
headers: HttpHeaders
|
|
||||||
status_code: int | None
|
|
||||||
content_length: int
|
|
||||||
|
|
||||||
|
|
||||||
class JsonTruncText(JsonBaseModel):
|
|
||||||
text: str
|
|
||||||
truncated: bool
|
|
||||||
|
|
||||||
|
|
||||||
class GraphQLOperation(JsonBaseModel):
|
|
||||||
name: JsonTruncText | None
|
|
||||||
operation_type: str | None
|
|
||||||
query: JsonTruncText | None
|
|
||||||
result: JsonTruncText | None
|
|
||||||
result_invalid: bool
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCallPayload(JsonBaseModel):
|
|
||||||
event_type: Literal["api_call"]
|
|
||||||
request: ApiCallRequest
|
|
||||||
response: ApiCallResponse
|
|
||||||
gql_operations: list[GraphQLOperation]
|
|
||||||
app: App | None
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttemptRequest(JsonBaseModel):
|
|
||||||
headers: HttpHeaders
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttemptResponse(JsonBaseModel):
|
|
||||||
headers: HttpHeaders
|
|
||||||
status_code: int | None
|
|
||||||
content_length: int
|
|
||||||
body: JsonTruncText
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryPayload(JsonBaseModel):
|
|
||||||
content_length: int
|
|
||||||
body: JsonTruncText
|
|
||||||
|
|
||||||
|
|
||||||
class EventDelivery(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
status: str
|
|
||||||
event_type: str
|
|
||||||
event_sync: bool
|
|
||||||
payload: EventDeliveryPayload
|
|
||||||
|
|
||||||
|
|
||||||
class Webhook(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
target_url: str
|
|
||||||
subscription_query: JsonTruncText | None
|
|
||||||
|
|
||||||
|
|
||||||
class EventDeliveryAttemptPayload(JsonBaseModel):
|
|
||||||
event_type: Literal["event_delivery_attempt"]
|
|
||||||
id: str
|
|
||||||
time: datetime
|
|
||||||
duration: float | None
|
|
||||||
status: str
|
|
||||||
next_retry: datetime | None
|
|
||||||
request: EventDeliveryAttemptRequest
|
|
||||||
response: EventDeliveryAttemptResponse
|
|
||||||
event_delivery: EventDelivery
|
|
||||||
webhook: Webhook
|
|
||||||
app: App
|
|
||||||
|
|
||||||
|
|
||||||
OBSERVABILITY_EVENTS = ApiCallPayload | EventDeliveryAttemptPayload | dict[str, Any]
|
|
|
@ -1,13 +0,0 @@
|
||||||
from .common import (
|
|
||||||
SALEOR_API_URL_HEADER,
|
|
||||||
SALEOR_SIGNATURE_HEADER,
|
|
||||||
SALEOR_TOKEN_HEADER,
|
|
||||||
SaleorToken,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"SALEOR_TOKEN_HEADER",
|
|
||||||
"SALEOR_API_URL_HEADER",
|
|
||||||
"SALEOR_SIGNATURE_HEADER",
|
|
||||||
"SaleorToken",
|
|
||||||
]
|
|
|
@ -1,103 +0,0 @@
|
||||||
from typing import Any, Dict, Optional, Sequence
|
|
||||||
from urllib.parse import urlparse, urlunparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
from .graphql import (
|
|
||||||
CREATE_WEBHOOK,
|
|
||||||
DELETE_PRIVATE_METADATA,
|
|
||||||
GET_APP_INFO,
|
|
||||||
UPDATE_PRIVATE_METADATA,
|
|
||||||
)
|
|
||||||
from .utils import JsonBaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookInfo(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
target_url: str
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfo(JsonBaseModel):
|
|
||||||
id: str
|
|
||||||
webhooks: list[WebhookInfo]
|
|
||||||
private_metafields: dict[str, str]
|
|
||||||
|
|
||||||
|
|
||||||
class GraphQLError(Exception):
|
|
||||||
"""
|
|
||||||
Raised on Saleor GraphQL errors
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
errors: Sequence[Dict[str, Any]],
|
|
||||||
response_data: Optional[Dict[str, Any]] = None,
|
|
||||||
):
|
|
||||||
self.errors = errors
|
|
||||||
self.response_data = response_data
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"GraphQLError: {', '.join([error['message'] for error in self.errors])}."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SaleorClient:
|
|
||||||
def __init__(self, saleor_api_url: str, user_agent, auth_token=None):
|
|
||||||
self.saleor_api_url = saleor_api_url
|
|
||||||
self.headers = {"User-Agent": user_agent}
|
|
||||||
if auth_token:
|
|
||||||
self.headers["Authorization"] = f"Bearer {auth_token}"
|
|
||||||
|
|
||||||
async def get_jwks(self) -> dict[str, Any]:
|
|
||||||
parts = urlparse(self.saleor_api_url)
|
|
||||||
jwks_url = urlunparse(parts._replace(path="/.well-known/jwks.json"))
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
res = await client.get(jwks_url)
|
|
||||||
return res.json()
|
|
||||||
|
|
||||||
async def execute(self, query, variables=None):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
data = {"query": query, "variables": variables}
|
|
||||||
res = await client.post(
|
|
||||||
self.saleor_api_url,
|
|
||||||
json=data,
|
|
||||||
headers=self.headers,
|
|
||||||
)
|
|
||||||
res_data = res.json()
|
|
||||||
if errors := res_data.get("errors"):
|
|
||||||
raise GraphQLError(errors=errors, response_data=res_data.get("data"))
|
|
||||||
return res_data["data"]
|
|
||||||
|
|
||||||
async def app_info(self) -> AppInfo:
|
|
||||||
result = await self.execute(GET_APP_INFO)
|
|
||||||
return AppInfo.parse_obj(result["app"])
|
|
||||||
|
|
||||||
async def update_private_metadata(self, app_id: str, metadata: dict[str, str]):
|
|
||||||
metadata_input = [{"key": key, "value": val} for key, val in metadata.items()]
|
|
||||||
await self.execute(
|
|
||||||
UPDATE_PRIVATE_METADATA,
|
|
||||||
variables={"appId": app_id, "metadata": metadata_input},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_private_metadata(self, app_id: str, keys: set[str]):
|
|
||||||
await self.execute(
|
|
||||||
DELETE_PRIVATE_METADATA, variables={"appId": app_id, "keys": list(keys)}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_webhook(
|
|
||||||
self,
|
|
||||||
target_url: str,
|
|
||||||
events: list[str],
|
|
||||||
name: str,
|
|
||||||
):
|
|
||||||
webhook_input = {
|
|
||||||
"targetUrl": target_url,
|
|
||||||
"events": events,
|
|
||||||
"name": name,
|
|
||||||
}
|
|
||||||
await self.execute(
|
|
||||||
CREATE_WEBHOOK,
|
|
||||||
variables={"input": webhook_input},
|
|
||||||
)
|
|
|
@ -1,151 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import Request
|
|
||||||
from jwt.api_jwk import PyJWKSet
|
|
||||||
from pydantic import AnyHttpUrl, BaseModel, Field
|
|
||||||
from starlette.routing import NoMatchFound
|
|
||||||
|
|
||||||
from ..utils import get_base_url
|
|
||||||
from .crypto import decode_jwt
|
|
||||||
|
|
||||||
SALEOR_API_URL_HEADER = "saleor-api-url"
|
|
||||||
SALEOR_TOKEN_HEADER = "authorization-bearer"
|
|
||||||
SALEOR_SIGNATURE_HEADER = "saleor-signature"
|
|
||||||
|
|
||||||
|
|
||||||
class SaleorAppError(Exception):
|
|
||||||
"""Generic Saleor App Error, all framework errros inherit from this"""
|
|
||||||
|
|
||||||
|
|
||||||
class InstallAppError(SaleorAppError):
|
|
||||||
"""Install App error"""
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(SaleorAppError):
|
|
||||||
"""App is misconfigured"""
|
|
||||||
|
|
||||||
|
|
||||||
class InstallData(BaseModel):
|
|
||||||
auth_token: str
|
|
||||||
|
|
||||||
|
|
||||||
class SaleorPermissions(str, Enum):
|
|
||||||
MANAGE_OBSERVABILITY = "MANAGE_OBSERVABILITY"
|
|
||||||
|
|
||||||
|
|
||||||
class LazyUrl(str):
|
|
||||||
"""
|
|
||||||
Used to declare a fully qualified url that is to be resolved when the
|
|
||||||
request is available.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_validators__(cls):
|
|
||||||
yield cls.validate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, v):
|
|
||||||
return v
|
|
||||||
|
|
||||||
def resolve(self):
|
|
||||||
return self.request.url_for(self.name)
|
|
||||||
|
|
||||||
def __call__(self, request: Request):
|
|
||||||
self.request = request
|
|
||||||
try:
|
|
||||||
return self.resolve()
|
|
||||||
except NoMatchFound:
|
|
||||||
raise ConfigurationError(
|
|
||||||
f"Failed to resolve a lazy url, check if an endpoint named '{self.name}' is defined."
|
|
||||||
) from None
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.name)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.name == other.name
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self.name == other.name)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"LazyURL('{self.name}')"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
class LazyAbsoluteUrl(LazyUrl):
|
|
||||||
def resolve(self):
|
|
||||||
base_url = get_base_url(self.request)
|
|
||||||
return str(base_url.replace(path=self.name))
|
|
||||||
|
|
||||||
|
|
||||||
class LazyPath(LazyUrl):
|
|
||||||
"""
|
|
||||||
Much like LazyUrl but resolves only to the path part of an url.
|
|
||||||
The lazy aspect of this class is very redundant but is built like so to
|
|
||||||
maintain the same usage as the LazyUrl class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def resolve(self):
|
|
||||||
return self.request.app.url_path_for(self.name)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"LazyPath('{self.name}')"
|
|
||||||
|
|
||||||
|
|
||||||
class Webhook(BaseModel):
|
|
||||||
name: str
|
|
||||||
async_events: list[str] = Field(..., alias="asyncEvents")
|
|
||||||
query: str
|
|
||||||
target_url: AnyHttpUrl | LazyUrl = Field(..., alias="targetUrl")
|
|
||||||
is_active: bool = Field(..., alias="isActive")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
|
|
||||||
class Manifest(BaseModel):
|
|
||||||
id: str
|
|
||||||
permissions: list[str]
|
|
||||||
name: str
|
|
||||||
author: str
|
|
||||||
version: str
|
|
||||||
about: str
|
|
||||||
extensions: list[Any] = []
|
|
||||||
webhooks: list[Webhook] = []
|
|
||||||
data_privacy: str = Field(..., alias="dataPrivacy")
|
|
||||||
data_privacy_url: AnyHttpUrl | LazyUrl = Field(..., alias="dataPrivacyUrl")
|
|
||||||
homepage_url: AnyHttpUrl | LazyUrl = Field(..., alias="homepageUrl")
|
|
||||||
support_url: AnyHttpUrl | LazyUrl = Field(..., alias="supportUrl")
|
|
||||||
configuration_url: AnyHttpUrl | LazyUrl | None = Field(
|
|
||||||
None, alias="configurationUrl"
|
|
||||||
)
|
|
||||||
app_url: AnyHttpUrl | LazyUrl = Field(..., alias="appUrl")
|
|
||||||
token_target_url: AnyHttpUrl | LazyUrl = Field(
|
|
||||||
LazyUrl("install"), alias="tokenTargetUrl"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
|
|
||||||
class SaleorToken:
|
|
||||||
def __init__(self, token_str: str, jwks: dict[str, Any]):
|
|
||||||
self.token_str = token_str
|
|
||||||
self.jwks = PyJWKSet.from_dict(jwks)
|
|
||||||
self.jwt = decode_jwt(self.token_str, self.jwks)
|
|
||||||
|
|
||||||
def is_staff_user(self) -> bool:
|
|
||||||
return self.jwt["is_staff"]
|
|
||||||
|
|
||||||
def validate_permission(self, permissions: str | list[str]) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate_user_permission(self, permissions: str | list[str]) -> bool:
|
|
||||||
return True
|
|
|
@ -1,66 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from jwt.api_jwk import PyJWKSet
|
|
||||||
from jwt.api_jws import PyJWS
|
|
||||||
from jwt.api_jwt import PyJWT
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoException(Exception):
|
|
||||||
"""
|
|
||||||
Base exception for the crypto module errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class JWKSKeyMissing(CryptoException):
|
|
||||||
"""
|
|
||||||
Raised when a requested kid is missing from a keyset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class KeyIDMissing(CryptoException):
|
|
||||||
"""
|
|
||||||
Raised when a JWT without a 'kid' header is received.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
jwt_global_obj = PyJWT(options={"verify_signature": True})
|
|
||||||
jws_global_obj = PyJWS(options={"verify_signature": True})
|
|
||||||
get_unverified_header = jws_global_obj.get_unverified_header
|
|
||||||
|
|
||||||
|
|
||||||
def get_kid(sig_header: Dict[str, str]):
|
|
||||||
try:
|
|
||||||
return sig_header["kid"]
|
|
||||||
except KeyError as err:
|
|
||||||
raise KeyIDMissing() from err
|
|
||||||
|
|
||||||
|
|
||||||
def get_key_from_jwks(kid: str, jwks: PyJWKSet):
|
|
||||||
try:
|
|
||||||
jwks_key = jwks[kid]
|
|
||||||
except KeyError as err:
|
|
||||||
raise JWKSKeyMissing(f"The JWKS does not hold the key: {kid}") from err
|
|
||||||
return jwks_key.key
|
|
||||||
|
|
||||||
|
|
||||||
def decode_webhook_payload(jws: str, jwks: PyJWKSet, webhook_payload: bytes):
|
|
||||||
sig_header = get_unverified_header(jws)
|
|
||||||
key = get_key_from_jwks(kid=get_kid(sig_header), jwks=jwks)
|
|
||||||
|
|
||||||
return jws_global_obj.decode(
|
|
||||||
jws,
|
|
||||||
algorithms=[sig_header["alg"]],
|
|
||||||
key=key,
|
|
||||||
detached_payload=webhook_payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def decode_jwt(jwt: str, jwks: PyJWKSet):
|
|
||||||
sig_header = get_unverified_header(jwt)
|
|
||||||
key = get_key_from_jwks(kid=get_kid(sig_header), jwks=jwks)
|
|
||||||
|
|
||||||
return jwt_global_obj.decode(
|
|
||||||
jwt,
|
|
||||||
algorithms=[sig_header["alg"]],
|
|
||||||
key=key,
|
|
||||||
)
|
|
|
@ -1,62 +0,0 @@
|
||||||
GET_APP_INFO = """
|
|
||||||
query GetAppInfo {
|
|
||||||
app {
|
|
||||||
id
|
|
||||||
webhooks {
|
|
||||||
id
|
|
||||||
targetUrl
|
|
||||||
isActive
|
|
||||||
}
|
|
||||||
privateMetafields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
UPDATE_PRIVATE_METADATA = """
|
|
||||||
mutation UpdatePrivateMetadata($appId: ID!, $metadata: [MetadataInput!]!) {
|
|
||||||
updatePrivateMetadata(id: $appId, input: $metadata) {
|
|
||||||
errors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
code
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
privateMetafields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
DELETE_PRIVATE_METADATA = """
|
|
||||||
mutation DeletePrivateMetadata($appId: ID!, $keys: [String!]!) {
|
|
||||||
deletePrivateMetadata(id: $appId, keys: $keys) {
|
|
||||||
errors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
code
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
privateMetafields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
CREATE_WEBHOOK = """
|
|
||||||
mutation WebhookCreate($input: WebhookCreateInput!) {
|
|
||||||
webhookCreate(input: $input) {
|
|
||||||
webhookErrors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
code
|
|
||||||
}
|
|
||||||
webhook {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
|
@ -1,40 +0,0 @@
|
||||||
from typing import Generic, Type, TypeVar
|
|
||||||
|
|
||||||
from .client import AppInfo, SaleorClient
|
|
||||||
from .metadata import BaseMetadata
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseMetadata)
|
|
||||||
|
|
||||||
|
|
||||||
class SaleorManager(Generic[T]):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
saleor_api_url: str,
|
|
||||||
app_id: str,
|
|
||||||
auth_token: str,
|
|
||||||
metadata_cls: Type[T],
|
|
||||||
user_agent="test",
|
|
||||||
):
|
|
||||||
self.client = SaleorClient(saleor_api_url, user_agent, auth_token)
|
|
||||||
self.app_id = app_id
|
|
||||||
self.metadata_cls = metadata_cls
|
|
||||||
|
|
||||||
async def get_app_info(self) -> AppInfo:
|
|
||||||
return await self.client.app_info()
|
|
||||||
|
|
||||||
async def get_metadata(self) -> T:
|
|
||||||
app_info = await self.get_app_info()
|
|
||||||
return self.metadata_cls.parse_obj(app_info.private_metafields)
|
|
||||||
|
|
||||||
async def save_private_metadata(self, metadata: T, include: set[str] | None = None):
|
|
||||||
exported = metadata.export(include)
|
|
||||||
await self.client.update_private_metadata(self.app_id, exported)
|
|
||||||
|
|
||||||
async def delete_private_metadata(self, keys: str | set[str]):
|
|
||||||
await self.client.delete_private_metadata(self.app_id, {"datadog"})
|
|
||||||
flatten_keys: set[str] = set()
|
|
||||||
if isinstance(keys, str):
|
|
||||||
keys = {keys}
|
|
||||||
for key in keys:
|
|
||||||
flatten_keys.update(self.metadata_cls.field_flatten(key))
|
|
||||||
await self.client.delete_private_metadata(self.app_id, flatten_keys)
|
|
|
@ -1,88 +0,0 @@
|
||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from pydantic import BaseModel, root_validator, validator
|
|
||||||
from pydantic.fields import ModelField
|
|
||||||
|
|
||||||
|
|
||||||
class JsonFieldsModel(BaseModel):
|
|
||||||
@validator("*", pre=True)
|
|
||||||
def parse_field(cls, v):
|
|
||||||
if isinstance(v, str):
|
|
||||||
try:
|
|
||||||
return json.loads(v)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class FlatMetadata(JsonFieldsModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMetadata(JsonFieldsModel):
|
|
||||||
_flat_fields: dict[str, tuple[str, str]] = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def field_flatten(cls, field: str | ModelField) -> set[str]:
|
|
||||||
flatten_set: set[str] = set()
|
|
||||||
if isinstance(field, str):
|
|
||||||
field = cls.__fields__[field]
|
|
||||||
if issubclass(field.type_, FlatMetadata):
|
|
||||||
for sub_field in field.type_.__fields__.values():
|
|
||||||
flat_key = f"{field.name}_{sub_field.name}"
|
|
||||||
flatten_set.add(flat_key)
|
|
||||||
else:
|
|
||||||
flatten_set.add(field.name)
|
|
||||||
return flatten_set
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_flat_fields_map(cls) -> dict[str, tuple[str, str]]:
|
|
||||||
flat_fields: dict[str, tuple[str, str]] = {}
|
|
||||||
for field in cls.__fields__.values():
|
|
||||||
if issubclass(field.type_, FlatMetadata):
|
|
||||||
for sub_field in field.type_.__fields__.values():
|
|
||||||
flat_key = f"{field.name}_{sub_field.name}"
|
|
||||||
flat_fields[flat_key] = field.name, sub_field.name
|
|
||||||
return flat_fields
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
cls._flat_fields = cls._get_flat_fields_map()
|
|
||||||
for field_name in cls.__fields__.keys():
|
|
||||||
if field_name in cls._flat_fields:
|
|
||||||
parent, subfield = cls._flat_fields[field_name]
|
|
||||||
raise ValueError(
|
|
||||||
f"{cls!r}: field {field_name} name conflict with flatten {parent}.{subfield}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _encode_field(field, value):
|
|
||||||
if isinstance(value, FlatMetadata):
|
|
||||||
result = {}
|
|
||||||
for key, val in value.dict().items():
|
|
||||||
result[f"{field.name}_{key}"] = jsonable_encoder(val)
|
|
||||||
return result
|
|
||||||
return {field.name: jsonable_encoder(value)}
|
|
||||||
|
|
||||||
def export(self, include: set | None = None) -> dict[str, str]:
|
|
||||||
metadata: dict[str, Any] = {}
|
|
||||||
fields = set(self.__fields__.keys())
|
|
||||||
if include is not None:
|
|
||||||
fields = fields & include
|
|
||||||
for field_name in fields:
|
|
||||||
field, value = self.__fields__[field_name], getattr(self, field_name)
|
|
||||||
metadata.update(self._encode_field(field, value))
|
|
||||||
return {key: json.dumps(val) for key, val in metadata.items()}
|
|
||||||
|
|
||||||
@root_validator(pre=True)
|
|
||||||
def unflatten(cls, values):
|
|
||||||
unflatten_values: dict[str, Any] = {}
|
|
||||||
for key, val in values.items():
|
|
||||||
try:
|
|
||||||
parent_name, field_name = cls._flat_fields[key]
|
|
||||||
unflatten_values.setdefault(parent_name, {})[field_name] = val
|
|
||||||
except KeyError:
|
|
||||||
unflatten_values[key] = val
|
|
||||||
return unflatten_values
|
|
|
@ -1,81 +0,0 @@
|
||||||
import pytest
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ..metadata import BaseMetadata, FlatMetadata
|
|
||||||
|
|
||||||
|
|
||||||
class Foo(BaseModel):
|
|
||||||
a: str
|
|
||||||
b: float | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Bar(BaseModel):
|
|
||||||
a: int
|
|
||||||
b: Foo | None = None
|
|
||||||
c: list[Foo] = []
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleConfig(FlatMetadata):
|
|
||||||
login: str
|
|
||||||
password: str
|
|
||||||
more: Bar | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata(BaseMetadata):
|
|
||||||
a: str | None = None
|
|
||||||
b: int | None = None
|
|
||||||
c: list[str] | None = None
|
|
||||||
d: str | None = None
|
|
||||||
e: Bar | None = None
|
|
||||||
f: ExampleConfig | None = None
|
|
||||||
g: list[Bar] = []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def metadata():
|
|
||||||
return Metadata(
|
|
||||||
a="a",
|
|
||||||
b=3,
|
|
||||||
c=["a", "b", "c"],
|
|
||||||
e=Bar(a=0, b=Foo(a="a"), c=[Foo(a="a"), Foo(a="b")]),
|
|
||||||
f=ExampleConfig(
|
|
||||||
login="admin",
|
|
||||||
password="admin",
|
|
||||||
more=Bar(
|
|
||||||
a=1,
|
|
||||||
b=Foo(
|
|
||||||
a="a",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
g=[Bar(a=0), Bar(a=1, b=Foo(a="a"))],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_to_metadata(metadata):
|
|
||||||
assert metadata.export() == {
|
|
||||||
"a": '"a"',
|
|
||||||
"b": "3",
|
|
||||||
"c": '["a", "b", "c"]',
|
|
||||||
"d": "null",
|
|
||||||
"e": '{"a": 0, "b": {"a": "a", "b": null}, "c": [{"a": "a", "b": null}, {"a": "b", "b": null}]}',
|
|
||||||
"f_login": '"admin"',
|
|
||||||
"f_password": '"admin"',
|
|
||||||
"f_more": '{"a": 1, "b": {"a": "a", "b": null}, "c": []}',
|
|
||||||
"g": '[{"a": 0, "b": null, "c": []}, {"a": 1, "b": {"a": "a", "b": null}, "c": []}]',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_metadata_export_reversible(metadata):
|
|
||||||
exported = metadata.export()
|
|
||||||
assert Metadata.parse_obj(exported) == metadata
|
|
||||||
|
|
||||||
|
|
||||||
def test_metadata_flatten_field(metadata):
|
|
||||||
assert metadata.field_flatten("a") == {"a"}
|
|
||||||
assert metadata.field_flatten("f") == {"f_login", "f_password", "f_more"}
|
|
||||||
assert metadata.field_flatten(metadata.__fields__["f"]) == {
|
|
||||||
"f_login",
|
|
||||||
"f_password",
|
|
||||||
"f_more",
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
def to_camel(snake_str: str) -> str:
|
|
||||||
components = snake_str.split("_")
|
|
||||||
return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:])
|
|
||||||
|
|
||||||
|
|
||||||
class JsonBaseModel(BaseModel):
|
|
||||||
class Config:
|
|
||||||
alias_generator = to_camel
|
|
||||||
allow_population_by_field_name = True
|
|
|
@ -1,62 +0,0 @@
|
||||||
interface Configuration {
|
|
||||||
active: Boolean!
|
|
||||||
error: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigurationError {
|
|
||||||
field: String
|
|
||||||
message: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
# Integration specific
|
|
||||||
|
|
||||||
# Datadog
|
|
||||||
enum DatadogSite{
|
|
||||||
US1
|
|
||||||
US3
|
|
||||||
US5
|
|
||||||
EU1
|
|
||||||
US1_FED
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataDogCredentials {
|
|
||||||
site: DatadogSite!
|
|
||||||
apiKeyLast4: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatadogConfig implements Configuration {
|
|
||||||
active: Boolean!
|
|
||||||
error: String
|
|
||||||
credentials: DataDogCredentials!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Integrations {
|
|
||||||
datadog: DatadogConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
input DataDogCredentialsInput {
|
|
||||||
site: DatadogSite!
|
|
||||||
apiKey: String!
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
input DatadogConfigInput {
|
|
||||||
active: Boolean
|
|
||||||
credentials: DataDogCredentialsInput
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataDogConfigMutationResult {
|
|
||||||
errors: [ConfigurationError!]!
|
|
||||||
datadog: DatadogConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
# End integrations
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
integrations: Integrations!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
updateDatadogConfig(input: DatadogConfigInput!): DataDogConfigMutationResult!
|
|
||||||
deleteDatadogConfig: DataDogConfigMutationResult!
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
from .integrations.datadog import DatadogCredentials
|
|
||||||
from .saleor.metadata import BaseMetadata, FlatMetadata
|
|
||||||
|
|
||||||
|
|
||||||
class DatadogConfig(FlatMetadata):
|
|
||||||
active: bool = False
|
|
||||||
error: str | None = None
|
|
||||||
credentials: DatadogCredentials
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata(BaseMetadata):
|
|
||||||
datadog: DatadogConfig | None = None
|
|
|
@ -1,43 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
|
||||||
|
|
||||||
from . import __version__ as app_version
|
|
||||||
from .saleor.common import LazyAbsoluteUrl, Manifest, SaleorPermissions
|
|
||||||
|
|
||||||
base_dir = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
class AppSettings(BaseSettings):
|
|
||||||
debug: bool = True
|
|
||||||
apl_url: str = f"file://{base_dir/'.fileApl.json'}"
|
|
||||||
mock_datadog_client = False
|
|
||||||
allowed_domains: set[str] = {"*"}
|
|
||||||
forbidden_domains: set[str] = set()
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
env_file_encoding = "utf-8"
|
|
||||||
|
|
||||||
|
|
||||||
settings = AppSettings()
|
|
||||||
|
|
||||||
|
|
||||||
manifest = Manifest(
|
|
||||||
id="saleor-app-monitoring",
|
|
||||||
name="Monitoring",
|
|
||||||
version=app_version,
|
|
||||||
about="Saleor Monitoring app",
|
|
||||||
data_privacy="",
|
|
||||||
app_url=LazyAbsoluteUrl("/"),
|
|
||||||
configuration_url=LazyAbsoluteUrl("/configuration"),
|
|
||||||
data_privacy_url="https://saleor.io/legal/privacy", # noqa
|
|
||||||
homepage_url="https://saleor.io/", # noqa
|
|
||||||
support_url="https://github.com/saleor", # noqa
|
|
||||||
token_target_url=LazyAbsoluteUrl("install"),
|
|
||||||
permissions=[SaleorPermissions.MANAGE_OBSERVABILITY],
|
|
||||||
extensions=[],
|
|
||||||
webhooks=[],
|
|
||||||
author="Saleor Commerce"
|
|
||||||
# TODO Add brand.logo.default
|
|
||||||
)
|
|
|
@ -1,20 +0,0 @@
|
||||||
import pytest
|
|
||||||
from httpx import AsyncClient
|
|
||||||
|
|
||||||
from ..app import app
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def anyio_backend():
|
|
||||||
return "asyncio", {"use_uvloop": True}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def observability_app():
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
async def client(observability_app):
|
|
||||||
async with AsyncClient(app=observability_app, base_url="http://test") as client:
|
|
||||||
yield client
|
|
|
@ -1,8 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_health(client):
|
|
||||||
response = await client.get("/health")
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == {"status": "ok"}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..utils import domain_validation
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"domain,allowed,forbidden,is_allowed",
|
|
||||||
[
|
|
||||||
("localhost:8000", {"*"}, None, True),
|
|
||||||
("wrong.com", {"example.com"}, None, False),
|
|
||||||
("sub.example.com", {"example.com"}, None, False),
|
|
||||||
("sub.example.com", {"*.example.com"}, None, True),
|
|
||||||
("sub.sub.example.com", {"*.example.com"}, None, True),
|
|
||||||
("example.com", {"*.example.com"}, None, False),
|
|
||||||
("sub.example.com", {"*.sub.example.com"}, None, False),
|
|
||||||
("sub.sub.example.com", {"*.sub.example.com"}, None, True),
|
|
||||||
("example.com", {"python.org", "example.com"}, None, True),
|
|
||||||
("any.com", {"python.org", "example.com", "*"}, None, True),
|
|
||||||
("example.com", {"*.example.com"}, None, False),
|
|
||||||
("sub.example.com", {"*"}, {"*.example.com"}, False),
|
|
||||||
("sub.example.com", {"*.example.com"}, {"*.banned.com"}, True),
|
|
||||||
("sub.example.com", {"*.example.com"}, {"x.example.com"}, True),
|
|
||||||
("non-eu-sub.saleor.cloud", {"*.saleor.cloud"}, {"*.eu.saleor.cloud"}, True),
|
|
||||||
("sub.eu.saleor.cloud", {"*.saleor.cloud"}, {"*.eu.saleor.cloud"}, False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_domain_validation(domain, allowed, forbidden, is_allowed):
|
|
||||||
assert domain_validation(domain, allowed, forbidden) is is_allowed
|
|
|
@ -1,48 +0,0 @@
|
||||||
import fnmatch
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
from fastapi import Request
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from starlette.datastructures import URL
|
|
||||||
|
|
||||||
HttpHeaders = list[tuple[str, str]]
|
|
||||||
|
|
||||||
|
|
||||||
def domain_validation(
|
|
||||||
domain_name: str, allowed: set[str], forbidden: set[str] | None = None
|
|
||||||
):
|
|
||||||
forbidden = set() if forbidden is None else forbidden
|
|
||||||
for origin in allowed:
|
|
||||||
if fnmatch.fnmatchcase(domain_name, origin):
|
|
||||||
for disallowed in forbidden:
|
|
||||||
if fnmatch.fnmatchcase(domain_name, disallowed):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def to_camel(snake_str: str) -> str:
|
|
||||||
components = snake_str.split("_")
|
|
||||||
return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:])
|
|
||||||
|
|
||||||
|
|
||||||
class JsonBaseModel(BaseModel):
|
|
||||||
class Config:
|
|
||||||
alias_generator = to_camel
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
|
|
||||||
def parse_headers(headers_list: HttpHeaders) -> dict[str, str]:
|
|
||||||
headers: dict[str, str] = {}
|
|
||||||
for key, val in headers_list:
|
|
||||||
headers[key] = val
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(request: Request) -> URL:
|
|
||||||
base_url, headers = request.base_url, request.headers
|
|
||||||
scheme = headers.get("x-forwarded-proto", base_url.scheme).split(",")[0].strip()
|
|
||||||
if forwarded_host := headers.get("x-forwarded-host"):
|
|
||||||
parts = urllib.parse.urlsplit(f"//{forwarded_host}")
|
|
||||||
base_url = base_url.replace(hostname=parts.hostname, port=parts.port)
|
|
||||||
return base_url.replace(scheme=scheme)
|
|
1925
apps/monitoring/backend/poetry.lock
generated
1925
apps/monitoring/backend/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,58 +0,0 @@
|
||||||
[tool.poetry]
|
|
||||||
name = "saleor-app-monitoring"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Saleor Monitoring app"
|
|
||||||
authors = ["Przemysław Łada <przemyslaw.lada@saleor.io>"]
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.10"
|
|
||||||
fastapi = "^0.92.0"
|
|
||||||
uvicorn = {extras = ["standard"], version = "^0.20.0"}
|
|
||||||
python-dotenv = "^0.21.0"
|
|
||||||
python-json-logger = "^2.0.4"
|
|
||||||
datadog-api-client = {extras = ["async"], version = "^2.8.0"}
|
|
||||||
ua-parser = "^0.16.1"
|
|
||||||
ariadne = "^0.17.1"
|
|
||||||
httpx = "^0.23.3"
|
|
||||||
pyjwt = {extras = ["crypto"], version = "^2.6.0"}
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
black = {extras = ["d"], version = "^22.12.0"}
|
|
||||||
pytest = "^7.2.0"
|
|
||||||
pre-commit = "^2.21.0"
|
|
||||||
ruff = "^0.0.215"
|
|
||||||
mypy = "^0.991"
|
|
||||||
anyio = "^3.6.2"
|
|
||||||
pytest-recording = "^0.12.2"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
target-version = "py310"
|
|
||||||
select = [
|
|
||||||
"E", # pycodestyle errors
|
|
||||||
"W", # pycodestyle warnings
|
|
||||||
"F", # pyflakes
|
|
||||||
"I001", # isort
|
|
||||||
"C", # flake8-comprehensions
|
|
||||||
"B", # flake8-bugbear
|
|
||||||
]
|
|
||||||
ignore = [
|
|
||||||
"E501", # line too long, handled by black
|
|
||||||
"B008", # do not perform function calls in argument defaults
|
|
||||||
"C901", # too complex
|
|
||||||
]
|
|
||||||
fix = true
|
|
||||||
fixable = ["I001"]
|
|
||||||
src = ["monitoring"]
|
|
||||||
|
|
||||||
[tool.ruff.isort]
|
|
||||||
known-first-party = ["monitoring"]
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
allow_redefinition = true
|
|
||||||
show_error_codes = true
|
|
||||||
check_untyped_defs = true
|
|
|
@ -1,18 +0,0 @@
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
target: dev
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
ports:
|
|
||||||
- "5001:80"
|
|
||||||
environment:
|
|
||||||
- DEBUG=True
|
|
||||||
# Uncomment to enable test credentials mode
|
|
||||||
# - MOCK_DATADOG_CLIENT=True
|
|
||||||
volumes:
|
|
||||||
- ./backend/monitoring/:/app/monitoring
|
|
5
apps/monitoring/next-env.d.ts
vendored
5
apps/monitoring/next-env.d.ts
vendored
|
@ -1,5 +0,0 @@
|
||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
|
@ -1,26 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
module.exports = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
|
|
||||||
rewrites() {
|
|
||||||
/**
|
|
||||||
* For dev/preview Next.js can work as a proxy and redirect unknown paths to provided backend address
|
|
||||||
*
|
|
||||||
* In production, when env is not provided, frontend will call its relative path and reverse proxy will do the rest
|
|
||||||
*/
|
|
||||||
const backendPath = process.env.MONITORING_APP_API_URL;
|
|
||||||
|
|
||||||
if(!backendPath) {
|
|
||||||
throw new Error('Please set MONITORING_APP_API_URL variable')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fallback: [
|
|
||||||
{
|
|
||||||
source: "/:path*",
|
|
||||||
destination: `${backendPath}/:path*`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"name": "saleor-app-monitoring",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"scripts": {
|
|
||||||
"build": "pnpm generate && next build",
|
|
||||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
|
||||||
"generate": "graphql-codegen",
|
|
||||||
"lint": "next lint",
|
|
||||||
"lint:fix": "eslint --fix .",
|
|
||||||
"start": "next start",
|
|
||||||
"test": "vitest"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@material-ui/core": "^4.12.4",
|
|
||||||
"@material-ui/icons": "^4.11.3",
|
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
|
||||||
"@saleor/app-sdk": "0.41.1",
|
|
||||||
"@saleor/apps-shared": "workspace:*",
|
|
||||||
"@saleor/apps-ui": "workspace:*",
|
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.95",
|
|
||||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
|
||||||
"@urql/exchange-auth": "^2.1.4",
|
|
||||||
"@vitejs/plugin-react": "4.0.0",
|
|
||||||
"clsx": "^1.2.1",
|
|
||||||
"graphql": "16.6.0",
|
|
||||||
"graphql-tag": "^2.12.6",
|
|
||||||
"jsdom": "^20.0.3",
|
|
||||||
"next": "13.4.8",
|
|
||||||
"pino": "^8.14.1",
|
|
||||||
"pino-pretty": "^10.0.0",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.43.9",
|
|
||||||
"urql": "^4.0.4",
|
|
||||||
"vite": "4.3.9",
|
|
||||||
"vitest": "0.31.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@graphql-codegen/cli": "3.2.2",
|
|
||||||
"@graphql-codegen/introspection": "3.0.1",
|
|
||||||
"@graphql-codegen/schema-ast": "^3.0.1",
|
|
||||||
"@graphql-codegen/typed-document-node": "3.0.2",
|
|
||||||
"@graphql-codegen/typescript": "3.0.2",
|
|
||||||
"@graphql-codegen/typescript-operations": "3.0.2",
|
|
||||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
|
||||||
"@types/react": "18.2.5",
|
|
||||||
"@types/react-dom": "18.2.5",
|
|
||||||
"eslint": "8.44.0",
|
|
||||||
"eslint-config-saleor": "workspace:*",
|
|
||||||
"typescript": "5.1.6"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 800.5 203.19" style="enable-background:new 0 0 800.5 203.19;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#632CA6;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M260.87,144.65h-37.4v-86.1h37.4c26.94,0,40.43,13.57,40.43,40.7C301.29,129.51,287.81,144.65,260.87,144.65z
|
|
||||||
M239.45,130.79h19c17.9,0,26.84-10.51,26.84-31.55c0-17.91-8.95-26.87-26.84-26.87h-19L239.45,130.79L239.45,130.79z"/>
|
|
||||||
<polygon class="st0" points="318.04,144.65 301.62,144.65 338.25,58.55 355.44,58.55 392.85,144.65 375.66,144.65 364.8,121.17
|
|
||||||
337.17,121.17 342.66,107.32 360.58,107.32 346.46,74.98 "/>
|
|
||||||
<polygon class="st0" points="383.82,58.55 449.28,58.55 449.28,72.39 424.55,72.39 424.55,144.65 408.57,144.65 408.57,72.39
|
|
||||||
383.82,72.39 "/>
|
|
||||||
<polygon class="st0" points="457.5,144.65 441.08,144.65 477.71,58.55 494.9,58.55 532.31,144.65 515.1,144.65 504.24,121.17
|
|
||||||
476.61,121.17 482.1,107.32 500.02,107.32 485.91,74.98 "/>
|
|
||||||
<path class="st0" d="M580.32,144.65h-37.4v-86.1h37.4c26.96,0,40.43,13.57,40.43,40.7C620.75,129.51,607.28,144.65,580.32,144.65z
|
|
||||||
M558.91,130.79h19c17.89,0,26.86-10.51,26.86-31.55c0-17.91-8.96-26.87-26.86-26.87h-19V130.79z"/>
|
|
||||||
<path class="st0" d="M631.58,101.72c0-29.2,14.45-43.79,43.33-43.79c28.44,0,42.64,14.59,42.64,43.79
|
|
||||||
c0,29.03-14.21,43.55-42.64,43.55C647.31,145.27,632.87,130.75,631.58,101.72z M674.91,131.39c17.36,0,26.05-10.01,26.05-30.05
|
|
||||||
c0-19.72-8.69-29.59-26.05-29.59c-17.82,0-26.73,9.87-26.73,29.59C648.18,121.38,657.09,131.39,674.91,131.39z"/>
|
|
||||||
<path class="st0" d="M784.26,109.81v20.16c-3.69,0.96-6.99,1.44-9.9,1.44c-19.55,0-29.31-10.34-29.31-31.01
|
|
||||||
c0-19.09,10.36-28.62,31.07-28.62c8.65,0,16.69,1.61,24.13,4.82V62.14c-7.44-2.8-15.89-4.21-25.34-4.21
|
|
||||||
c-30.97,0-46.46,14.15-46.46,42.47c0,29.9,15.22,44.87,45.67,44.87c10.47,0,19.17-1.52,26.13-4.58V95.64h-25.82l-5.4,14.16
|
|
||||||
L784.26,109.81L784.26,109.81z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M158.87,144.16L142,133.04l-14.07,23.5l-16.36-4.78l-14.41,21.99l0.74,6.92l78.33-14.43l-4.55-48.94
|
|
||||||
L158.87,144.16z M85.82,123.07l12.57-1.73c2.03,0.91,3.45,1.26,5.89,1.88c3.8,0.99,8.19,1.94,14.7-1.34
|
|
||||||
c1.51-0.75,4.67-3.64,5.94-5.28l51.49-9.34l5.25,63.57l-88.21,15.9L85.82,123.07z M181.46,100.16l-5.08,0.97L166.62,0.25
|
|
||||||
L0.25,19.54l20.5,166.33l19.47-2.83c-1.55-2.22-3.98-4.91-8.11-8.35c-5.74-4.76-3.71-12.86-0.32-17.97
|
|
||||||
c4.47-8.63,27.54-19.61,26.23-33.41c-0.47-5.02-1.27-11.55-5.93-16.03c-0.17,1.86,0.14,3.65,0.14,3.65s-1.91-2.44-2.87-5.77
|
|
||||||
c-0.95-1.28-1.69-1.68-2.7-3.39c-0.72,1.97-0.62,4.26-0.62,4.26s-1.56-3.7-1.82-6.82c-0.93,1.4-1.16,4.05-1.16,4.05
|
|
||||||
s-2.03-5.83-1.57-8.97c-0.93-2.73-3.68-8.15-2.9-20.47c5.08,3.56,16.26,2.71,20.61-3.71c1.45-2.13,2.44-7.93-0.72-19.36
|
|
||||||
c-2.03-7.33-7.05-18.25-9.01-22.4l-0.23,0.17c1.03,3.34,3.16,10.33,3.98,13.73c2.47,10.29,3.13,13.87,1.97,18.61
|
|
||||||
c-0.99,4.12-3.35,6.82-9.35,9.84c-6,3.03-13.96-4.34-14.47-4.74c-5.83-4.64-10.34-12.22-10.84-15.9
|
|
||||||
c-0.52-4.03,2.32-6.45,3.76-9.74c-2.05,0.59-4.34,1.63-4.34,1.63s2.73-2.83,6.1-5.27c1.4-0.92,2.21-1.51,3.68-2.73
|
|
||||||
c-2.13-0.03-3.86,0.02-3.86,0.02s3.55-1.92,7.23-3.31c-2.69-0.12-5.27-0.02-5.27-0.02S35.75,27.1,42,24.5
|
|
||||||
c4.3-1.76,8.5-1.24,10.86,2.17c3.1,4.47,6.35,6.9,13.25,8.41c4.24-1.88,5.52-2.84,10.84-4.29c4.68-5.15,8.36-5.82,8.36-5.82
|
|
||||||
s-1.82,1.67-2.31,4.3c2.66-2.09,5.57-3.84,5.57-3.84s-1.13,1.39-2.18,3.6l0.24,0.36c3.1-1.86,6.74-3.32,6.74-3.32
|
|
||||||
s-1.04,1.32-2.26,3.02c2.34-0.02,7.08,0.1,8.91,0.31c10.86,0.24,13.11-11.6,17.28-13.08c5.22-1.86,7.55-2.99,16.44,5.74
|
|
||||||
c7.63,7.5,13.59,20.91,10.63,23.92c-2.48,2.49-7.38-0.97-12.8-7.74c-2.87-3.58-5.03-7.81-6.05-13.19
|
|
||||||
c-0.86-4.54-4.19-7.17-4.19-7.17s1.93,4.31,1.93,8.11c0,2.08,0.26,9.84,3.59,14.19c-0.33,0.64-0.48,3.15-0.85,3.63
|
|
||||||
c-3.87-4.68-12.19-8.03-13.54-9.02c4.59,3.76,15.14,12.4,19.19,20.68c3.83,7.83,1.57,15.01,3.51,16.87
|
|
||||||
c0.55,0.53,8.24,10.11,9.72,14.93c2.58,8.39,0.15,17.21-3.22,22.68l-9.43,1.47c-1.38-0.38-2.31-0.58-3.55-1.29
|
|
||||||
c0.68-1.21,2.04-4.22,2.05-4.84l-0.53-0.93c-2.94,4.16-7.85,8.2-11.94,10.52c-5.35,3.03-11.51,2.56-15.52,1.32
|
|
||||||
c-11.39-3.51-22.16-11.21-24.75-13.23c0,0-0.08,1.61,0.41,1.98c2.87,3.24,9.45,9.1,15.81,13.18l-13.55,1.49l6.41,49.89
|
|
||||||
c-2.84,0.41-3.28,0.61-6.39,1.05c-2.74-9.68-7.98-16.01-13.71-19.69c-5.05-3.25-12.02-3.98-18.7-2.66l-0.43,0.5
|
|
||||||
c4.64-0.48,10.12,0.19,15.74,3.75c5.52,3.49,9.97,12.51,11.61,17.94c2.1,6.94,3.55,14.36-2.1,22.23
|
|
||||||
c-4.02,5.59-15.74,8.68-25.22,2c2.53,4.07,5.95,7.4,10.55,8.02c6.84,0.93,13.33-0.26,17.79-4.84c3.81-3.92,5.84-12.12,5.3-20.75
|
|
||||||
l6.03-0.87l2.18,15.49l99.88-12.03L181.46,100.16z M120.69,58.08c-0.28,0.64-0.72,1.05-0.06,3.12l0.04,0.12l0.1,0.27l0.27,0.62
|
|
||||||
c1.19,2.42,2.49,4.71,4.66,5.88c0.56-0.09,1.15-0.16,1.75-0.19c2.04-0.09,3.33,0.23,4.15,0.68c0.07-0.41,0.09-1,0.04-1.88
|
|
||||||
c-0.16-3.07,0.61-8.29-5.29-11.04c-2.23-1.03-5.35-0.72-6.39,0.58c0.19,0.02,0.36,0.06,0.49,0.11
|
|
||||||
C122.04,56.89,120.98,57.43,120.69,58.08 M137.23,86.73c-0.77-0.43-4.39-0.26-6.93,0.04c-4.84,0.57-10.07,2.25-11.22,3.14
|
|
||||||
c-2.08,1.61-1.14,4.42,0.4,5.57c4.32,3.22,8.1,5.39,12.09,4.86c2.45-0.32,4.61-4.2,6.14-7.73
|
|
||||||
C138.77,90.19,138.77,87.58,137.23,86.73 M94.36,61.88c1.37-1.3-6.8-3-13.14,1.32c-4.67,3.19-4.82,10.03-0.35,13.9
|
|
||||||
c0.45,0.38,0.82,0.66,1.16,0.88c1.31-0.62,2.8-1.24,4.51-1.79c2.9-0.94,5.3-1.43,7.28-1.68c0.95-1.06,2.05-2.92,1.77-6.29
|
|
||||||
C95.22,63.63,91.75,64.36,94.36,61.88"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 5.5 KiB |
|
@ -1,52 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 800.5 196.2" style="enable-background:new 0 0 800.5 196.2;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M167.2,98.4l-54.9,10c-1.4,1.8-4.7,4.8-6.3,5.6c-6.9,3.5-11.6,2.5-15.7,1.4c-2.6-0.7-4.1-1-6.3-2l-13.4,1.8
|
|
||||||
l8.1,67.9l94.1-17L167.2,98.4z M83.4,176.7l-0.8-7.4L98,145.9l17.4,5.1l15-25.1l18,11.9l13.7-28.7l4.9,52.2L83.4,176.7z M79.5,110
|
|
||||||
c-6.8-4.4-13.8-10.6-16.9-14.1c-0.5-0.4-0.4-2.1-0.4-2.1c2.8,2.2,14.3,10.4,26.4,14.1c4.3,1.3,10.9,1.8,16.6-1.4
|
|
||||||
c4.4-2.5,9.6-6.8,12.7-11.2l0.6,1c0,0.7-1.5,3.9-2.2,5.2c1.3,0.8,2.3,1,3.8,1.4l10.1-1.6c3.6-5.8,6.2-15.2,3.4-24.2
|
|
||||||
c-1.6-5.1-9.8-15.4-10.4-15.9c-2.1-2,0.3-9.6-3.7-18c-4.3-8.8-15.6-18-20.5-22.1c1.4,1,10.3,4.6,14.4,9.6c0.4-0.5,0.6-3.2,0.9-3.9
|
|
||||||
c-3.6-4.6-3.8-12.9-3.8-15.1c0-4.1-2.1-8.7-2.1-8.7s3.6,2.8,4.5,7.7c1.1,5.7,3.4,10.3,6.5,14.1c5.8,7.2,11,10.9,13.7,8.2
|
|
||||||
c3.2-3.2-3.2-17.5-11.3-25.5c-9.5-9.3-12-8.1-17.5-6.1c-4.4,1.5-6.8,14.2-18.4,13.9c-2-0.2-7-0.4-9.5-0.3c1.3-1.8,2.4-3.2,2.4-3.2
|
|
||||||
s-3.9,1.6-7.2,3.6l-0.3-0.4c1.1-2.4,2.3-3.8,2.3-3.8s-3.1,1.9-5.9,4.1c0.5-2.8,2.5-4.6,2.5-4.6s-3.9,0.7-8.9,6.2
|
|
||||||
c-5.7,1.5-7,2.6-11.6,4.6c-7.4-1.6-10.8-4.2-14.1-9c-2.5-3.6-7-4.2-11.6-2.3c-6.7,2.8-15.1,6.5-15.1,6.5s2.8-0.1,5.6,0
|
|
||||||
c-3.9,1.5-7.7,3.5-7.7,3.5s1.8-0.1,4.1,0c-1.6,1.3-2.4,1.9-3.9,2.9c-3.6,2.6-6.5,5.6-6.5,5.6s2.4-1.1,4.6-1.7
|
|
||||||
c-1.5,3.5-4.6,6.1-4,10.4c0.5,3.9,5.3,12,11.6,17c0.5,0.4,9,8.3,15.4,5.1c6.4-3.2,8.9-6.1,10-10.5c1.2-5.1,0.5-8.9-2.1-19.9
|
|
||||||
c-0.9-3.6-3.1-11.1-4.2-14.6l0.2-0.2c2.1,4.4,7.4,16.1,9.6,23.9c3.4,12.2,2.3,18.4,0.8,20.7c-4.7,6.8-16.6,7.8-22,4
|
|
||||||
c-0.8,13.1,2.1,18.9,3.1,21.8c-0.5,3.3,1.7,9.6,1.7,9.6s0.2-2.8,1.2-4.3c0.3,3.3,1.9,7.3,1.9,7.3s-0.1-2.4,0.7-4.5
|
|
||||||
c1.1,1.8,1.9,2.2,2.9,3.6c1,3.6,3,6.2,3,6.2s-0.3-1.9-0.2-3.9c5,4.8,5.8,11.8,6.3,17.1c1.4,14.7-23.2,26.4-28,35.6
|
|
||||||
c-3.6,5.4-5.8,14.1,0.3,19.2c14.8,12.3,9.1,15.7,16.5,21.1c10.2,7.4,22.9,4.1,27.2-1.9c6-8.4,4.5-16.3,2.2-23.7
|
|
||||||
c-1.8-5.8-6.5-15.4-12.4-19.1c-6-3.8-11.9-4.5-16.8-4l0.5-0.5c7.1-1.4,14.6-0.6,20,2.8c6.1,3.9,11.7,10.7,14.6,21
|
|
||||||
c3.3-0.5,3.8-0.7,6.8-1.1L65,111.6L79.5,110z M113.8,43.3c6.3,2.9,5.5,8.5,5.6,11.8c0.1,0.9,0,1.6-0.1,2c-0.9-0.5-2.2-0.8-4.4-0.7
|
|
||||||
c-0.6,0-1.3,0.1-1.9,0.2c-2.3-1.2-3.7-3.7-5-6.3c-0.1-0.2-0.2-0.5-0.3-0.7c0-0.1-0.1-0.2-0.1-0.3c0,0,0-0.1,0-0.1
|
|
||||||
c-0.7-2.2-0.2-2.7,0.1-3.3s1.4-1.3-0.2-1.8c-0.1,0-0.3-0.1-0.5-0.1C108.1,42.6,111.4,42.2,113.8,43.3z M106,79.9
|
|
||||||
c1.2-0.9,6.8-2.7,12-3.4c2.7-0.3,6.6-0.5,7.4,0c1.6,0.9,1.6,3.7,0.5,6.3c-1.6,3.8-3.9,7.9-6.6,8.2c-4.3,0.6-8.3-1.7-12.9-5.2
|
|
||||||
C104.8,84.6,103.8,81.6,106,79.9z M65.6,51.4c6.8-4.6,15.5-2.8,14-1.4c-2.8,2.7,0.9,1.9,1.3,6.8c0.3,3.6-0.9,5.6-1.9,6.7
|
|
||||||
c-2.1,0.3-4.7,0.8-7.8,1.8c-1.8,0.6-3.4,1.2-4.8,1.9c-0.4-0.2-0.8-0.5-1.2-0.9C60.5,62.1,60.7,54.8,65.6,51.4z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st1" d="M249.2,141.8H211v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C290.5,126.3,276.8,141.8,249.2,141.8z M227.4,127.6
|
|
||||||
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.1-27.4-27.4-27.4h-19.4V127.6L227.4,127.6z"/>
|
|
||||||
<polygon class="st1" points="307.6,141.8 290.9,141.8 328.3,53.8 345.9,53.8 384.1,141.8 366.5,141.8 355.4,117.8 327.2,117.8
|
|
||||||
332.8,103.7 351.1,103.7 336.7,70.6 "/>
|
|
||||||
<polygon class="st1" points="374.8,53.8 441.7,53.8 441.7,68 416.5,68 416.5,141.8 400.1,141.8 400.1,68 374.8,68 "/>
|
|
||||||
<polygon class="st1" points="450.1,141.8 433.3,141.8 470.8,53.8 488.3,53.8 526.5,141.8 509,141.8 497.9,117.8 469.6,117.8
|
|
||||||
475.2,103.7 493.5,103.7 479.1,70.6 "/>
|
|
||||||
<path class="st1" d="M575.6,141.8h-38.2v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C616.9,126.3,603.1,141.8,575.6,141.8z M553.7,127.6
|
|
||||||
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.2-27.4-27.4-27.4h-19.4V127.6z"/>
|
|
||||||
<path class="st1" d="M628,97.9c0-29.8,14.8-44.7,44.3-44.7c29,0,43.6,14.9,43.6,44.7c0,29.7-14.5,44.5-43.6,44.5
|
|
||||||
C644,142.4,629.3,127.6,628,97.9z M672.2,128.2c17.7,0,26.6-10.2,26.6-30.7c0-20.2-8.9-30.2-26.6-30.2
|
|
||||||
c-18.2,0-27.3,10.1-27.3,30.2C644.9,118,654,128.2,672.2,128.2z"/>
|
|
||||||
<path class="st1" d="M783.9,106.2v20.6c-3.8,1-7.1,1.5-10.1,1.5c-20,0-30-10.6-30-31.7c0-19.5,10.6-29.2,31.7-29.2
|
|
||||||
c8.8,0,17,1.6,24.7,4.9V57.5c-7.6-2.9-16.2-4.3-25.9-4.3c-31.6,0-47.5,14.4-47.5,43.4c0,30.6,15.5,45.8,46.7,45.8
|
|
||||||
c10.7,0,19.6-1.6,26.7-4.7v-46h-26.4l-5.5,14.5H783.9z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.4 KiB |
|
@ -1,18 +0,0 @@
|
||||||
<svg width="409" height="137" viewBox="0 0 409 137" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.17 95.75C164.97 97.06 159.17 97.63 156.32 97.63C148.33 97.63 143.52 93.47 143.52 86.38V50.32L138.79 49.83V39.48H158.24V82.46C158.24 84.91 159.67 85.97 162.52 85.97H165.54L167.17 95.75Z" fill="#002E42"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M242.2 65.25H237.39C232.82 65.25 231.35 66.31 231.35 70.06C231.35 73.81 232.98 75.36 237.39 75.36C241.87 75.36 243.26 73.73 243.26 69.98C243.26 67.69 242.85 66.23 242.2 65.25ZM256.15 65.25C257.7 66.8 258.11 68.76 258.11 70.71C258.11 79.27 251.83 83.76 237.31 83.76C235.27 83.76 231.84 83.52 231.76 83.52C230.93 83.52 230.75 84 230.69 84.19C230.12 86.13 231.68 86.42 235.68 87.12L247.17 88.82C256.72 90.29 259.08 94.77 259.08 100.41C259.08 107.75 255.09 113.13 236.9 113.13C230.54 113.13 223.44 112.4 217.49 111.42V101.47C224.66 102.04 230.36 102.53 237.05 102.53C242.51 102.53 244.57 102.36 244.57 100.33C244.65 98.7 242.72 98.22 238.45 97.51L227.92 95.85C221.07 94.62 219.03 90.71 219.19 86.96C219.27 84.68 220.41 82.47 222.37 81.09C218.05 78.81 216.09 75.06 216.09 69.75C216.09 59.15 223.84 56.05 237.46 56.05H264.03V65.27H256.15V65.25Z" fill="#002E42"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M349.8 56.29H342.8H337.89V59.54V59.69V65.74V68.99H342.8V86.47V96.83H357.57V86.47V56.29H349.8Z" fill="#002E42"/>
|
|
||||||
<path d="M348.42 36.24C353.01 36.24 356.23 39.38 356.23 43.8C356.23 48.13 353 51.27 348.42 51.27C343.92 51.27 340.69 48.13 340.69 43.8C340.69 39.38 343.92 36.24 348.42 36.24Z" fill="#002E42"/>
|
|
||||||
<path d="M324.6 81.84C329.19 81.84 332.41 84.98 332.41 89.4C332.41 93.73 329.18 96.87 324.6 96.87C320.1 96.87 316.87 93.73 316.87 89.4C316.87 84.98 320.1 81.84 324.6 81.84Z" fill="#002E42"/>
|
|
||||||
<path d="M190.07 54.89C177.3 54.89 168.14 63.8 168.14 76.34C168.14 88.63 177.3 97.55 190.07 97.55C203.08 97.55 212.24 88.63 212.24 76.34C212.24 63.8 203.08 54.89 190.07 54.89ZM190.14 85.16C184.79 85.16 180.95 81.42 180.95 76.27C180.95 71.02 184.79 67.28 190.14 67.28C195.59 67.28 199.43 71.02 199.43 76.27C199.43 81.42 195.59 85.16 190.14 85.16Z" fill="#002E42"/>
|
|
||||||
<path d="M386 55.15C373.23 55.15 364.07 64.06 364.07 76.6C364.07 88.89 373.23 97.81 386 97.81C399.01 97.81 408.17 88.89 408.17 76.6C408.18 64.06 399.02 55.15 386 55.15ZM386.07 85.42C380.72 85.42 376.88 81.68 376.88 76.53C376.88 71.28 380.72 67.54 386.07 67.54C391.52 67.54 395.36 71.28 395.36 76.53C395.36 81.68 391.53 85.42 386.07 85.42Z" fill="#002E42"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M306.48 96.49H267.49V86.45L289.43 65.9H278.75H278.1H270.15V55.95H306.07V65.9L284.29 86.45L295.96 86.53V81.56H306.48V96.49Z" fill="#002E42"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 102.13L58.97 136.18L117.94 102.13V34.04L58.97 0L2.95 32.35C1.82 33 1.12 34.22 1.12 35.52C1.12 36.82 1.82 38.04 2.95 38.69L56.42 69.56V96.53L0 63.96V102.13ZM58.96 130.29L5.09 99.19V72.78L29.144 86.6631L29.14 86.67L58.77 104L89.86 86.06V50.17L58.78 32.23L30.3702 48.6309L7.64 35.51L58.97 5.88L112.83 36.99V99.19L58.96 130.29ZM35.4647 51.5717L58.97 65.14L82.2835 51.6749L58.78 38.11L35.4647 51.5717ZM84.77 56.1216L61.52 69.55V96.5348L84.77 83.12V56.1216Z" fill="#053447"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0">
|
|
||||||
<path d="M0 0H408.18V136.18H0V0Z" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.3 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 819 158.97"><defs><style>.cls-1{fill:#00ac69;}.cls-2{fill:#1ce783;}.cls-3{fill:#1d252c;}</style></defs><polygon class="cls-1" points="111.19 55.03 111.19 103.94 68.84 128.4 68.84 158.97 137.68 119.23 137.68 39.74 111.19 55.03"/><polygon class="cls-2" points="68.84 30.58 111.19 55.03 137.68 39.74 137.68 39.74 68.84 0 0 39.74 0 39.74 26.48 55.03 68.84 30.58"/><polygon class="cls-3" points="42.36 94.78 42.36 143.69 68.84 158.97 68.84 79.49 0 39.74 0 70.32 42.36 94.78"/><path class="cls-3" d="M242.17,50.14c-14.82,0-21.84,9.36-21.84,9.36h-.78L218,51.7H200.05v79.56h19.5v-46c0-10.14,7-17.16,17.16-17.16s17.16,7,17.16,17.16v46h19.5V83.68C273.37,63.4,260.11,50.14,242.17,50.14Z" transform="translate(-10 -10.51)"/><polygon class="cls-3" points="442.47 93.45 441.35 93.45 428.09 41.19 408.25 41.19 394.99 93.45 393.88 93.45 380.61 41.19 360.33 41.19 380.61 120.75 404.36 120.75 417.61 69.27 418.73 69.27 431.99 120.75 455.73 120.75 476.01 41.19 455.73 41.19 442.47 93.45"/><path class="cls-3" d="M545.72,58.72h-.78l-1.56-7H527v79.55h19.5v-46c0-10.14,4.68-14.82,14.82-14.82h10V51.71h-11.6A17.56,17.56,0,0,0,545.72,58.72Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M614.47,50.14c-23.39,0-40.55,17.16-40.55,41.34s16.19,41.34,40.55,41.34c19.73,0,31.61-11.61,36.56-20.15l-17.9-6.38c-1.77,3.24-8.91,9.47-18.66,9.47-11.37,0-19.49-7.12-21.05-18h59.27a35.38,35.38,0,0,0,.78-7.8C653.47,67.3,636.31,50.14,614.47,50.14ZM593.42,84.46c2.34-10.14,9.36-17.94,21.05-17.94,10.93,0,17.94,7.8,19.5,17.94Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M326.4,50.14c-23.4,0-40.56,17.16-40.56,41.34S302,132.82,326.4,132.82c19.73,0,31.6-11.61,36.55-20.15l-17.9-6.38c-1.77,3.24-8.9,9.47-18.65,9.47-11.37,0-19.5-7.12-21.06-18h59.28a35.38,35.38,0,0,0,.78-7.8C365.4,67.3,348.24,50.14,326.4,50.14ZM305.34,84.46c2.34-10.14,9.36-17.94,21.06-17.94,10.92,0,17.94,7.8,19.5,17.94Z" transform="translate(-10 -10.51)"/><rect class="cls-3" x="692.14" y="9.78" width="19.5" height="19.5"/><path class="cls-3" d="M775.45,114.88c-11.7,0-21.06-9.36-21.06-23.4s9.36-23.4,21.06-23.4,16.38,7.8,17.94,12.48l17.66-6.28c-4.28-11.11-14.78-24.14-35.6-24.14-23.4,0-40.56,17.16-40.56,41.34s17.16,41.34,40.56,41.34c21,0,31.5-13.24,35.7-24.88l-17.76-6.32C791.83,107.08,787.15,114.88,775.45,114.88Z" transform="translate(-10 -10.51)"/><polygon class="cls-3" points="645.63 27.11 656.7 27.11 656.7 120.75 676.2 120.75 676.2 9.78 645.63 9.78 645.63 27.11"/><rect class="cls-3" x="692.14" y="41.19" width="19.5" height="79.56"/><path class="cls-3" d="M821.59,116a7.52,7.52,0,1,0,7.41,7.52A7.28,7.28,0,0,0,821.59,116Zm0,13.89a6.37,6.37,0,1,1,6.26-6.37A6.12,6.12,0,0,1,821.59,129.85Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M824.82,122.13a2.64,2.64,0,0,0-2.82-2.62h-3.34v7.84h1.15v-2.72h1.05l2.71,2.72H825l-2.71-2.72A2.53,2.53,0,0,0,824.82,122.13Zm-5,1.35v-2.82H822a1.5,1.5,0,0,1,1.68,1.47c0,.83-.53,1.35-1.68,1.35Z" transform="translate(-10 -10.51)"/></svg>
|
|
Before Width: | Height: | Size: 3 KiB |
|
@ -1,17 +0,0 @@
|
||||||
import { DatadogSite } from "../generated/graphql";
|
|
||||||
|
|
||||||
export const API_KEYS_LINKS: { [key in DatadogSite]: string } = {
|
|
||||||
[DatadogSite.Us1]: "https://app.datadoghq.com/organization-settings/api-keys",
|
|
||||||
[DatadogSite.Us3]: "https://us3.datadoghq.com/organization-settings/api-keys",
|
|
||||||
[DatadogSite.Us5]: "https://us5.datadoghq.com/organization-settings/api-keys",
|
|
||||||
[DatadogSite.Eu1]: "https://app.datadoghq.eu/organization-settings/api-keys",
|
|
||||||
[DatadogSite.Us1Fed]: "https://app.ddog-gov.com/organization-settings/api-keys",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DATADOG_SITES_LINKS: { [key in DatadogSite]: string } = {
|
|
||||||
[DatadogSite.Us1]: "https://datadoghq.com",
|
|
||||||
[DatadogSite.Us3]: "https://us3.datadoghq.com",
|
|
||||||
[DatadogSite.Us5]: "https://us5.datadoghq.com",
|
|
||||||
[DatadogSite.Eu1]: "https://datadoghq.eu",
|
|
||||||
[DatadogSite.Us1Fed]: "https://ddog-gov.com",
|
|
||||||
};
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { cacheExchange, createClient as urqlCreateClient, fetchExchange, Provider } from "urql";
|
|
||||||
import { authExchange } from "@urql/exchange-auth";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local client creation. Contrary to other apps, Monitoring frontend doesnt contact Saleor directly,
|
|
||||||
* but calls Python-based service which also provides graphQL endpoint.
|
|
||||||
*
|
|
||||||
* App calls /graphql/ which is rewritten MONITORING_APP_API_URL. See next.config.js
|
|
||||||
*/
|
|
||||||
const createGraphQLClient = ({
|
|
||||||
graphql,
|
|
||||||
saleorApiUrl,
|
|
||||||
token,
|
|
||||||
}: {
|
|
||||||
graphql: string;
|
|
||||||
saleorApiUrl: string;
|
|
||||||
token: string;
|
|
||||||
}) => {
|
|
||||||
return urqlCreateClient({
|
|
||||||
url: graphql,
|
|
||||||
exchanges: [
|
|
||||||
cacheExchange,
|
|
||||||
authExchange(async (utils) => {
|
|
||||||
return {
|
|
||||||
addAuthToOperation(operation) {
|
|
||||||
const headers: Record<string, string> = token
|
|
||||||
? {
|
|
||||||
"Authorization-Bearer": token,
|
|
||||||
"Saleor-Api-Url": saleorApiUrl,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return utils.appendHeaders(operation, headers);
|
|
||||||
},
|
|
||||||
didAuthError(error) {
|
|
||||||
return error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN");
|
|
||||||
},
|
|
||||||
async refreshAuth() {},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
fetchExchange,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function GraphQLProvider(props: PropsWithChildren<{}>) {
|
|
||||||
const { appBridgeState } = useAppBridge();
|
|
||||||
const saleorApiUrl = appBridgeState?.saleorApiUrl!;
|
|
||||||
const token = appBridgeState?.token!;
|
|
||||||
|
|
||||||
const client = createGraphQLClient({
|
|
||||||
saleorApiUrl,
|
|
||||||
token,
|
|
||||||
graphql: "/graphql/",
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Provider value={client} {...props} />;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { useTheme } from "@saleor/macaw-ui/next";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
// todo move to shared
|
|
||||||
export function ThemeSynchronizer() {
|
|
||||||
const { appBridgeState } = useAppBridge();
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!setTheme || !appBridgeState?.theme) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appBridgeState.theme === "light") {
|
|
||||||
setTheme("defaultLight");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appBridgeState.theme === "dark") {
|
|
||||||
setTheme("defaultDark");
|
|
||||||
}
|
|
||||||
}, [appBridgeState?.theme, setTheme]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import "@saleor/macaw-ui/next/style";
|
|
||||||
import "../style.css";
|
|
||||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import React from "react";
|
|
||||||
import { AppProps } from "next/app";
|
|
||||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
|
||||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
|
||||||
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
|
||||||
import { NoSSRWrapper } from "@saleor/apps-shared";
|
|
||||||
import { GraphQLProvider } from "../graphql-provider";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure instance is a singleton.
|
|
||||||
*/
|
|
||||||
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
|
||||||
|
|
||||||
function NextApp({ Component, pageProps }: AppProps) {
|
|
||||||
return (
|
|
||||||
<NoSSRWrapper>
|
|
||||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
|
||||||
<GraphQLProvider>
|
|
||||||
<ThemeProvider>
|
|
||||||
<ThemeSynchronizer />
|
|
||||||
<RoutePropagator />
|
|
||||||
<Box padding={4}>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</Box>
|
|
||||||
</ThemeProvider>
|
|
||||||
</GraphQLProvider>
|
|
||||||
</AppBridgeProvider>
|
|
||||||
</NoSSRWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NextApp;
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { NoProvidersConfigured } from "../../ui/no-providers-configured";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { DatadogConfig } from "../../ui/datadog/datadog-config";
|
|
||||||
import { DatadogSite, useConfigQuery } from "../../../generated/graphql";
|
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { DATADOG_SITES_LINKS } from "../../datadog-urls";
|
|
||||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
|
||||||
import { Breadcrumbs, TextLink } from "@saleor/apps-ui";
|
|
||||||
|
|
||||||
const useActiveProvider = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const selectedProvider = router.query?.path && router.query.path[0];
|
|
||||||
|
|
||||||
return selectedProvider ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfigurationPage = () => {
|
|
||||||
const [configuration, fetchConfiguration] = useConfigQuery();
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
|
|
||||||
const datadogCredentials = configuration.data?.integrations.datadog?.credentials;
|
|
||||||
const datadogError = configuration.data?.integrations.datadog?.error;
|
|
||||||
|
|
||||||
const { push } = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConfiguration();
|
|
||||||
}, [fetchConfiguration]);
|
|
||||||
|
|
||||||
const selectedProvider = useActiveProvider();
|
|
||||||
|
|
||||||
if (configuration.fetching && !configuration.data) {
|
|
||||||
return <Text>Loading...</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedProvider === "datadog") {
|
|
||||||
return <DatadogConfig />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!configuration.data?.integrations.datadog) {
|
|
||||||
return <NoProvidersConfigured />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// when configured and everything is fine
|
|
||||||
if (datadogCredentials && !datadogError) {
|
|
||||||
const site = configuration.data?.integrations.datadog?.credentials.site ?? DatadogSite.Us1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
|
||||||
<Text as={"h1"} variant="heading">
|
|
||||||
App configured
|
|
||||||
</Text>
|
|
||||||
<Text as={"p"}>
|
|
||||||
Visit{" "}
|
|
||||||
<TextLink newTab href={DATADOG_SITES_LINKS[site] ?? "https://app.datadoghq.com/"}>
|
|
||||||
Datadog
|
|
||||||
</TextLink>{" "}
|
|
||||||
to access your logs
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
push("/configuration/datadog");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit configuration
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (datadogError) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Text variant="heading" as={"h1"}>
|
|
||||||
Configuration Error
|
|
||||||
</Text>
|
|
||||||
<Text color={"textCriticalDefault"}>{datadogError}</Text>
|
|
||||||
<Button
|
|
||||||
marginTop={8}
|
|
||||||
onClick={() => {
|
|
||||||
push("/configuration/datadog");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit configuration
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfigurationPage;
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { isInIframe } from "@saleor/apps-shared";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is page publicly accessible from your app.
|
|
||||||
* You should probably remove it.
|
|
||||||
*/
|
|
||||||
const IndexPage: NextPage = () => {
|
|
||||||
const { appBridgeState } = useAppBridge();
|
|
||||||
const { replace } = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (appBridgeState?.ready) {
|
|
||||||
replace("/configuration");
|
|
||||||
}
|
|
||||||
}, [appBridgeState?.ready, replace]);
|
|
||||||
|
|
||||||
if (isInIframe()) {
|
|
||||||
return <Text>Loading...</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Text variant="heading" as="h1">
|
|
||||||
Saleor Monitoring
|
|
||||||
</Text>
|
|
||||||
<Text>Install App in Saleor Dashboard to use it</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IndexPage;
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.2 KiB |
|
@ -1,6 +0,0 @@
|
||||||
/**
|
|
||||||
* Add test setup logic here
|
|
||||||
*
|
|
||||||
* https://vitest.dev/config/#setupfiles
|
|
||||||
*/
|
|
||||||
export {};
|
|
|
@ -1,3 +0,0 @@
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
DataDogCredentialsInput,
|
|
||||||
DatadogSite,
|
|
||||||
useConfigQuery,
|
|
||||||
useDeleteDatadogCredentialsMutation,
|
|
||||||
useUpdateCredentialsMutation,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
import { ArrowLeftIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
|
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import DatadogLogo from "../../assets/datadog/dd_logo_h_rgb.svg";
|
|
||||||
import { gql } from "urql";
|
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { API_KEYS_LINKS } from "../../datadog-urls";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
import { Input, Select, Toggle } from "@saleor/react-hook-form-macaw";
|
|
||||||
import { Breadcrumbs } from "@saleor/apps-ui";
|
|
||||||
|
|
||||||
gql`
|
|
||||||
query Config {
|
|
||||||
integrations {
|
|
||||||
datadog {
|
|
||||||
error
|
|
||||||
active
|
|
||||||
credentials {
|
|
||||||
apiKeyLast4
|
|
||||||
site
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
mutation UpdateCredentials($input: DatadogConfigInput!) {
|
|
||||||
updateDatadogConfig(input: $input) {
|
|
||||||
datadog {
|
|
||||||
error
|
|
||||||
active
|
|
||||||
credentials {
|
|
||||||
apiKeyLast4
|
|
||||||
site
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errors {
|
|
||||||
message
|
|
||||||
field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
gql`
|
|
||||||
mutation DeleteDatadogCredentials {
|
|
||||||
deleteDatadogConfig {
|
|
||||||
errors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
}
|
|
||||||
datadog {
|
|
||||||
credentials {
|
|
||||||
site
|
|
||||||
apiKeyLast4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const buildMaskedKey = (keyLastChars: string) => `************${keyLastChars}`;
|
|
||||||
|
|
||||||
const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
|
|
||||||
const url = API_KEYS_LINKS[site];
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
Get one{" "}
|
|
||||||
<a
|
|
||||||
href={url}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: url,
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DatadogConfig = () => {
|
|
||||||
const [queryData, fetchConfig] = useConfigQuery();
|
|
||||||
const [, mutateCredentials] = useUpdateCredentialsMutation();
|
|
||||||
const [, deleteCredentials] = useDeleteDatadogCredentialsMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const { notifyError, notifySuccess } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { register, handleSubmit, setValue, control, reset, watch } = useForm<
|
|
||||||
DataDogCredentialsInput & {
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
defaultValues: {
|
|
||||||
site: DatadogSite.Us1,
|
|
||||||
apiKey: "",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeSite = watch("site");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConfig();
|
|
||||||
}, [fetchConfig]);
|
|
||||||
|
|
||||||
const updateValuesToCurrentConfig = () => {
|
|
||||||
const datadogConfig = queryData.data?.integrations.datadog;
|
|
||||||
|
|
||||||
if (!datadogConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue("active", datadogConfig.active);
|
|
||||||
setValue("apiKey", buildMaskedKey(datadogConfig.credentials.apiKeyLast4));
|
|
||||||
setValue("site", datadogConfig.credentials.site);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const datadogConfig = queryData.data?.integrations.datadog;
|
|
||||||
|
|
||||||
if (datadogConfig) {
|
|
||||||
updateValuesToCurrentConfig();
|
|
||||||
}
|
|
||||||
}, [queryData.data, setValue]);
|
|
||||||
|
|
||||||
if (queryData.fetching && !queryData.data) {
|
|
||||||
return <Text>Loading</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Breadcrumbs.Item href={"/configuration"}>Configuration</Breadcrumbs.Item>
|
|
||||||
<Breadcrumbs.Item>DataDog</Breadcrumbs.Item>
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
<Box marginTop={8} display={"grid"} __gridTemplateColumns={"400px auto"} gap={8}>
|
|
||||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
|
||||||
<Text variant={"heading"} as={"h1"}>
|
|
||||||
Configuration
|
|
||||||
</Text>
|
|
||||||
<Image width={100} src={DatadogLogo} alt="DataDog" />
|
|
||||||
<Text as={"p"}>
|
|
||||||
Configure your Datadog integration to send your Saleor metrics to Datadog.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
display={"flex"}
|
|
||||||
gap={4}
|
|
||||||
flexDirection={"column"}
|
|
||||||
as={"form"}
|
|
||||||
borderColor={"neutralHighlight"}
|
|
||||||
borderWidth={1}
|
|
||||||
borderStyle={"solid"}
|
|
||||||
borderRadius={4}
|
|
||||||
padding={8}
|
|
||||||
onSubmit={handleSubmit((values) => {
|
|
||||||
return mutateCredentials({
|
|
||||||
input: {
|
|
||||||
active: values.active,
|
|
||||||
credentials: {
|
|
||||||
apiKey: values.apiKey,
|
|
||||||
site: values.site,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).then((res) => {
|
|
||||||
const updatedConfig = res.data?.updateDatadogConfig.datadog;
|
|
||||||
const errors = res.data?.updateDatadogConfig.errors;
|
|
||||||
|
|
||||||
if (updatedConfig) {
|
|
||||||
setValue("active", updatedConfig.active);
|
|
||||||
setValue("apiKey", buildMaskedKey(updatedConfig.credentials.apiKeyLast4));
|
|
||||||
setValue("site", updatedConfig.credentials.site);
|
|
||||||
|
|
||||||
notifySuccess("Configuration updated", "Successfully updated Datadog settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors?.length) {
|
|
||||||
notifyError("Error configuring Datadog", errors[0].message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Box as={"label"} display={"flex"} gap={2}>
|
|
||||||
<Toggle control={control} name={"active"} />
|
|
||||||
<Text variant={"bodyEmp"}>Active</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label={"Datadog Site"}
|
|
||||||
options={Object.values(DatadogSite).map((v) => ({
|
|
||||||
label: v,
|
|
||||||
value: v,
|
|
||||||
}))}
|
|
||||||
control={control}
|
|
||||||
name={"site"}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="API Key"
|
|
||||||
defaultValue=""
|
|
||||||
helperText={<ApiKeyHelperText site={activeSite} />}
|
|
||||||
control={control}
|
|
||||||
name={"apiKey"}
|
|
||||||
/>
|
|
||||||
{queryData.data?.integrations.datadog?.error && (
|
|
||||||
<Text color={"textCriticalDefault"}>{queryData.data?.integrations.datadog?.error}</Text>
|
|
||||||
)}
|
|
||||||
<Box display={"flex"} gap={2} marginTop={8} justifyContent={"flex-end"}>
|
|
||||||
<Button
|
|
||||||
variant={"tertiary"}
|
|
||||||
type="reset"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
deleteCredentials({}).then(() => {
|
|
||||||
fetchConfig();
|
|
||||||
reset();
|
|
||||||
notifySuccess("Configuration updated", "Successfully deleted Datadog settings");
|
|
||||||
router.push("/configuration");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text color={"textCriticalDefault"}>Delete configuration</Text>
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">Save configuration</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const NoProvidersConfigured = () => (
|
|
||||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
|
||||||
<Text as={"h1"} variant="heading">
|
|
||||||
No providers configured
|
|
||||||
</Text>
|
|
||||||
<Text as={"p"}>You need to configure Datadog to enable the app</Text>
|
|
||||||
<Link href={"/configuration/datadog"}>
|
|
||||||
<Button>Configure Datadog</Button>
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
);
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true
|
|
||||||
},
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://turbo.build/schema.json",
|
|
||||||
"extends": ["//"],
|
|
||||||
"pipeline": {
|
|
||||||
"build": {
|
|
||||||
"env": [
|
|
||||||
"APL",
|
|
||||||
"APP_DEBUG",
|
|
||||||
"NODE_ENV",
|
|
||||||
"SECRET_KEY",
|
|
||||||
"ALLOWED_DOMAIN_PATTERN",
|
|
||||||
"REST_APL_ENDPOINT",
|
|
||||||
"REST_APL_TOKEN",
|
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
|
||||||
"SENTRY_DSN",
|
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
|
||||||
"SENTRY_ORG",
|
|
||||||
"SENTRY_PROJECT",
|
|
||||||
"SENTRY_AUTH_TOKEN",
|
|
||||||
"NEXT_PUBLIC_VERCEL_ENV",
|
|
||||||
"MONITORING_APP_API_URL",
|
|
||||||
"APP_IFRAME_BASE_URL",
|
|
||||||
"APP_API_BASE_URL"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
test: {
|
|
||||||
passWithNoTests: true,
|
|
||||||
environment: "jsdom",
|
|
||||||
setupFiles: "./src/setup-tests.ts",
|
|
||||||
css: false,
|
|
||||||
},
|
|
||||||
});
|
|
118
pnpm-lock.yaml
118
pnpm-lock.yaml
|
@ -1017,124 +1017,6 @@ importers:
|
||||||
specifier: 5.1.6
|
specifier: 5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
|
|
||||||
apps/monitoring:
|
|
||||||
dependencies:
|
|
||||||
'@material-ui/core':
|
|
||||||
specifier: ^4.12.4
|
|
||||||
version: 4.12.4(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@material-ui/icons':
|
|
||||||
specifier: ^4.11.3
|
|
||||||
version: 4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@material-ui/lab':
|
|
||||||
specifier: 4.0.0-alpha.61
|
|
||||||
version: 4.0.0-alpha.61(@material-ui/core@4.12.4)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@saleor/app-sdk':
|
|
||||||
specifier: 0.41.1
|
|
||||||
version: 0.41.1(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@saleor/apps-shared':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/shared
|
|
||||||
'@saleor/apps-ui':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/ui
|
|
||||||
'@saleor/macaw-ui':
|
|
||||||
specifier: 0.8.0-pre.95
|
|
||||||
version: 0.8.0-pre.95(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@saleor/react-hook-form-macaw':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/react-hook-form-macaw
|
|
||||||
'@urql/exchange-auth':
|
|
||||||
specifier: ^2.1.4
|
|
||||||
version: 2.1.4(graphql@16.6.0)
|
|
||||||
'@vitejs/plugin-react':
|
|
||||||
specifier: 4.0.0
|
|
||||||
version: 4.0.0(vite@4.3.9)
|
|
||||||
clsx:
|
|
||||||
specifier: ^1.2.1
|
|
||||||
version: 1.2.1
|
|
||||||
graphql:
|
|
||||||
specifier: 16.6.0
|
|
||||||
version: 16.6.0
|
|
||||||
graphql-tag:
|
|
||||||
specifier: ^2.12.6
|
|
||||||
version: 2.12.6(graphql@16.6.0)
|
|
||||||
jsdom:
|
|
||||||
specifier: ^20.0.3
|
|
||||||
version: 20.0.3
|
|
||||||
next:
|
|
||||||
specifier: 13.4.8
|
|
||||||
version: 13.4.8(@babel/core@7.22.8)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
pino:
|
|
||||||
specifier: ^8.14.1
|
|
||||||
version: 8.14.1
|
|
||||||
pino-pretty:
|
|
||||||
specifier: ^10.0.0
|
|
||||||
version: 10.0.0
|
|
||||||
react:
|
|
||||||
specifier: 18.2.0
|
|
||||||
version: 18.2.0
|
|
||||||
react-dom:
|
|
||||||
specifier: 18.2.0
|
|
||||||
version: 18.2.0(react@18.2.0)
|
|
||||||
react-hook-form:
|
|
||||||
specifier: ^7.43.9
|
|
||||||
version: 7.44.3(react@18.2.0)
|
|
||||||
urql:
|
|
||||||
specifier: ^4.0.4
|
|
||||||
version: 4.0.4(graphql@16.6.0)(react@18.2.0)
|
|
||||||
vite:
|
|
||||||
specifier: 4.3.9
|
|
||||||
version: 4.3.9(@types/node@18.15.3)
|
|
||||||
vitest:
|
|
||||||
specifier: 0.31.3
|
|
||||||
version: 0.31.3(jsdom@20.0.3)
|
|
||||||
devDependencies:
|
|
||||||
'@graphql-codegen/cli':
|
|
||||||
specifier: 3.2.2
|
|
||||||
version: 3.2.2(@babel/core@7.22.8)(@types/node@18.15.3)(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/introspection':
|
|
||||||
specifier: 3.0.1
|
|
||||||
version: 3.0.1(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/schema-ast':
|
|
||||||
specifier: ^3.0.1
|
|
||||||
version: 3.0.1(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/typed-document-node':
|
|
||||||
specifier: 3.0.2
|
|
||||||
version: 3.0.2(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/typescript':
|
|
||||||
specifier: 3.0.2
|
|
||||||
version: 3.0.2(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/typescript-operations':
|
|
||||||
specifier: 3.0.2
|
|
||||||
version: 3.0.2(graphql@16.6.0)
|
|
||||||
'@graphql-codegen/typescript-urql':
|
|
||||||
specifier: 3.7.3
|
|
||||||
version: 3.7.3(graphql-tag@2.12.6)(graphql@16.6.0)
|
|
||||||
'@graphql-typed-document-node/core':
|
|
||||||
specifier: 3.2.0
|
|
||||||
version: 3.2.0(graphql@16.6.0)
|
|
||||||
'@testing-library/react':
|
|
||||||
specifier: ^13.4.0
|
|
||||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@testing-library/react-hooks':
|
|
||||||
specifier: ^8.0.1
|
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@types/react':
|
|
||||||
specifier: 18.2.5
|
|
||||||
version: 18.2.5
|
|
||||||
'@types/react-dom':
|
|
||||||
specifier: 18.2.5
|
|
||||||
version: 18.2.5
|
|
||||||
eslint:
|
|
||||||
specifier: 8.44.0
|
|
||||||
version: 8.44.0
|
|
||||||
eslint-config-saleor:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/eslint-config-saleor
|
|
||||||
typescript:
|
|
||||||
specifier: 5.1.6
|
|
||||||
version: 5.1.6
|
|
||||||
|
|
||||||
apps/products-feed:
|
apps/products-feed:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
|
|
Loading…
Reference in a new issue