Merge pull request #1968 from Unleash/merge-frontend-with-backend-2
Merge frontend with backend
@ -9,3 +9,5 @@
|
||||
!CHANGELOG.md
|
||||
!LICENSE
|
||||
!README.md
|
||||
!frontend
|
||||
frontend/node_modules
|
||||
|
@ -10,3 +10,4 @@ website/core
|
||||
website/pages
|
||||
website
|
||||
setupJest.js
|
||||
frontend
|
||||
|
1
.github/workflows/build_coverage.yaml
vendored
@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
paths-ignore:
|
||||
- website/**
|
||||
- frontend/**
|
||||
- coverage/**
|
||||
|
||||
jobs:
|
||||
|
25
.github/workflows/build_frontend_prs.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: PR -> Frontend Build & Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- frontend/**
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn run test
|
||||
- run: yarn run fmt:check
|
2
.github/workflows/build_prs.yaml
vendored
@ -17,6 +17,6 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'yarn'
|
||||
- run: yarn
|
||||
- run: yarn install --frozen-lockfile --ignore-scripts
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
|
11
.github/workflows/release.yaml
vendored
@ -28,3 +28,14 @@ jobs:
|
||||
npm publish --tag ${TAG:-latest}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
- name: Publish static assets to S3
|
||||
run: |
|
||||
aws s3 cp frontend/build s3://getunleash-static/unleash/${{ steps.get_version.outputs.VERSION }} --recursive
|
||||
|
4
.gitignore
vendored
@ -23,10 +23,6 @@ coverage/lcov-report
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# webpack output
|
||||
packages/unleash-frontend/public/bundle.js
|
||||
packages/unleash-frontend/public/bundle.js.map
|
||||
|
||||
# liquibase stuff
|
||||
/sql
|
||||
unleash-db.jar
|
||||
|
@ -7,7 +7,7 @@ WORKDIR /unleash
|
||||
|
||||
COPY . /unleash
|
||||
|
||||
RUN yarn install --frozen-lockfile --ignore-scripts && yarn run build && yarn run local:package
|
||||
RUN yarn install --frozen-lockfile && yarn run local:package
|
||||
|
||||
WORKDIR /unleash/docker
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
"@passport-next/passport-google-oauth2": "^1.0.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"unleash-server": "file:./../build/"
|
||||
"unleash-server": "file:../build"
|
||||
},
|
||||
"resolutions": {
|
||||
"async": "^3.2.3",
|
||||
|
799
docker/yarn.lock
16
frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
25
frontend/.github/workflows/e2e.feature.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: e2e:feature
|
||||
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on: [deployment_status]
|
||||
jobs:
|
||||
e2e:
|
||||
# only runs this job on successful deploy
|
||||
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: |
|
||||
echo "$GITHUB_CONTEXT"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Cypress
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all
|
||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||
record: true
|
||||
spec: cypress/integration/feature/feature.spec.ts
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
25
frontend/.github/workflows/e2e.groups.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: e2e:groups
|
||||
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on: [deployment_status]
|
||||
jobs:
|
||||
e2e:
|
||||
# only runs this job on successful deploy
|
||||
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: |
|
||||
echo "$GITHUB_CONTEXT"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Cypress
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all
|
||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||
record: true
|
||||
spec: cypress/integration/groups/groups.spec.ts
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
25
frontend/.github/workflows/e2e.project-access.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: e2e:project-access
|
||||
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on: [deployment_status]
|
||||
jobs:
|
||||
e2e:
|
||||
# only runs this job on successful deploy
|
||||
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: |
|
||||
echo "$GITHUB_CONTEXT"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Cypress
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all
|
||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||
record: true
|
||||
spec: cypress/integration/projects/access/project-access.spec.ts
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
25
frontend/.github/workflows/e2e.segments.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: e2e:segments
|
||||
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on: [deployment_status]
|
||||
jobs:
|
||||
e2e:
|
||||
# only runs this job on successful deploy
|
||||
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: |
|
||||
echo "$GITHUB_CONTEXT"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Cypress
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all
|
||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||
record: true
|
||||
spec: cypress/integration/segments/segments.spec.ts
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
56
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
package-lock.json
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
typings/
|
||||
|
||||
# Built
|
||||
dist
|
||||
build
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
.DS_Store
|
||||
|
||||
cypress/downloads/*
|
||||
cypress/videos/*
|
||||
cypress/downloads/*
|
||||
cypress/screenshots/*
|
||||
.env.local
|
1
frontend/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
14.20
|
3
frontend/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
.github/*
|
||||
/src/openapi
|
||||
CHANGELOG.md
|
7
frontend/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80
|
||||
}
|
68
frontend/README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# frontend
|
||||
|
||||
This directory contains the Unleash Admin UI frontend app.
|
||||
|
||||
## Run with a local instance of the unleash-api
|
||||
|
||||
First, start the unleash-api backend on port 4242.
|
||||
Then, start the frontend dev server:
|
||||
|
||||
```
|
||||
cd ~/frontend
|
||||
yarn install
|
||||
yarn run start
|
||||
```
|
||||
|
||||
## Run with a heroku-hosted instance of unleash-api
|
||||
|
||||
Alternatively, instead of running unleash-api on localhost, use a remote instance:
|
||||
|
||||
```
|
||||
cd ~/frontend
|
||||
yarn install
|
||||
yarn run start:heroku
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
We have a set of Cypress tests that run on the build before a PR can be merged
|
||||
so it's important that you check these yourself before submitting a PR.
|
||||
On the server the tests will run against the deployed Heroku app so this is what you probably want to test against:
|
||||
|
||||
```
|
||||
yarn run start:heroku
|
||||
```
|
||||
|
||||
In a different shell, you can run the tests themselves:
|
||||
|
||||
```
|
||||
yarn run e2e:heroku
|
||||
```
|
||||
|
||||
If you need to test against patches against a local server instance,
|
||||
you'll need to run that, and then run the end to end tests using:
|
||||
|
||||
```
|
||||
yarn run e2e
|
||||
```
|
||||
|
||||
You may also need to test that a feature works against the enterprise version of unleash.
|
||||
Assuming the Heroku instance is still running, this can be done by:
|
||||
|
||||
```
|
||||
yarn run start:enterprise
|
||||
yarn run e2e
|
||||
```
|
||||
|
||||
## Generating the OpenAPI client
|
||||
|
||||
The frontend uses an OpenAPI client generated from the backend's OpenAPI spec.
|
||||
Whenever there are changes to the backend API, the client should be regenerated:
|
||||
|
||||
```
|
||||
./scripts/generate-openapi.sh
|
||||
```
|
||||
|
||||
This script assumes that you have a running instance of the enterprise backend at `http://localhost:4242`.
|
||||
The new OpenAPI client will be generated from the runtime schema of this instance.
|
||||
The target URL can be changed by setting the `UNLEASH_OPENAPI_URL` env var.
|
7
frontend/cypress.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"projectId": "tc2qff",
|
||||
"defaultCommandTimeout": 12000,
|
||||
"screenshotOnRunFailure": false,
|
||||
"video": false,
|
||||
"experimentalSessionAndOrigin": true
|
||||
}
|
5
frontend/cypress/fixtures/example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
331
frontend/cypress/integration/feature/feature.spec.ts
Normal file
@ -0,0 +1,331 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export {};
|
||||
|
||||
const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE'));
|
||||
const randomId = String(Math.random()).split('.')[1];
|
||||
const featureToggleName = `unleash-e2e-${randomId}`;
|
||||
const baseUrl = Cypress.config().baseUrl;
|
||||
const variant1 = 'variant1';
|
||||
const variant2 = 'variant2';
|
||||
let strategyId = '';
|
||||
|
||||
// Disable the prod guard modal by marking it as seen.
|
||||
const disableFeatureStrategiesProdGuard = () => {
|
||||
localStorage.setItem(
|
||||
'useFeatureStrategyProdGuardSettings:v2',
|
||||
JSON.stringify({ hide: true })
|
||||
);
|
||||
};
|
||||
|
||||
// Disable all active splash pages by visiting them.
|
||||
const disableActiveSplashScreens = () => {
|
||||
cy.visit(`/splash/operators`);
|
||||
};
|
||||
|
||||
describe('feature', () => {
|
||||
before(() => {
|
||||
disableFeatureStrategiesProdGuard();
|
||||
disableActiveSplashScreens();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${baseUrl}/api/admin/features/${featureToggleName}`,
|
||||
});
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${baseUrl}/api/admin/archive/${featureToggleName}`,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('can create a feature toggle', () => {
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
|
||||
cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/projects/default/features').as(
|
||||
'createFeature'
|
||||
);
|
||||
|
||||
cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName);
|
||||
cy.get("[data-testid='CF_DESC_ID'").type('hello-world');
|
||||
cy.get("[data-testid='CF_CREATE_BTN_ID']").click();
|
||||
cy.wait('@createFeature');
|
||||
cy.url().should('include', featureToggleName);
|
||||
});
|
||||
|
||||
it('gives an error if a toggle exists with the same name', () => {
|
||||
cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/projects/default/features').as(
|
||||
'createFeature'
|
||||
);
|
||||
|
||||
cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName);
|
||||
cy.get("[data-testid='CF_DESC_ID'").type('hello-world');
|
||||
cy.get("[data-testid='CF_CREATE_BTN_ID']").click();
|
||||
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
|
||||
'A toggle with that name already exists'
|
||||
);
|
||||
});
|
||||
|
||||
it('gives an error if a toggle name is url unsafe', () => {
|
||||
cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/projects/default/features').as(
|
||||
'createFeature'
|
||||
);
|
||||
|
||||
cy.get("[data-testid='CF_NAME_ID'").type('featureToggleUnsafe####$#//');
|
||||
cy.get("[data-testid='CF_DESC_ID'").type('hello-world');
|
||||
cy.get("[data-testid='CF_CREATE_BTN_ID']").click();
|
||||
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
|
||||
`"name" must be URL friendly`
|
||||
);
|
||||
});
|
||||
|
||||
it('can add a gradual rollout strategy to the development environment', () => {
|
||||
cy.visit(
|
||||
`/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=flexibleRollout`
|
||||
);
|
||||
|
||||
if (ENTERPRISE) {
|
||||
cy.get('[data-testid=ADD_CONSTRAINT_ID]').click();
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
}
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
|
||||
req => {
|
||||
expect(req.body.name).to.equal('flexibleRollout');
|
||||
expect(req.body.parameters.groupId).to.equal(featureToggleName);
|
||||
expect(req.body.parameters.stickiness).to.equal('default');
|
||||
expect(req.body.parameters.rollout).to.equal('50');
|
||||
|
||||
if (ENTERPRISE) {
|
||||
expect(req.body.constraints.length).to.equal(1);
|
||||
} else {
|
||||
expect(req.body.constraints.length).to.equal(0);
|
||||
}
|
||||
|
||||
req.continue(res => {
|
||||
strategyId = res.body.id;
|
||||
});
|
||||
}
|
||||
).as('addStrategyToFeature');
|
||||
|
||||
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
|
||||
cy.wait('@addStrategyToFeature');
|
||||
});
|
||||
|
||||
it('can update a strategy in the development environment', () => {
|
||||
cy.visit(
|
||||
`/projects/default/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}`
|
||||
);
|
||||
|
||||
cy.get('[data-testid=FLEXIBLE_STRATEGY_STICKINESS_ID]')
|
||||
.first()
|
||||
.click()
|
||||
.get('[data-testid=SELECT_ITEM_ID-sessionId')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-testid=FLEXIBLE_STRATEGY_GROUP_ID]')
|
||||
.first()
|
||||
.clear()
|
||||
.type('new-group-id');
|
||||
|
||||
cy.intercept(
|
||||
'PUT',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
|
||||
req => {
|
||||
expect(req.body.parameters.groupId).to.equal('new-group-id');
|
||||
expect(req.body.parameters.stickiness).to.equal('sessionId');
|
||||
expect(req.body.parameters.rollout).to.equal('50');
|
||||
|
||||
if (ENTERPRISE) {
|
||||
expect(req.body.constraints.length).to.equal(1);
|
||||
} else {
|
||||
expect(req.body.constraints.length).to.equal(0);
|
||||
}
|
||||
|
||||
req.continue(res => {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
});
|
||||
}
|
||||
).as('updateStrategy');
|
||||
|
||||
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
|
||||
cy.wait('@updateStrategy');
|
||||
});
|
||||
|
||||
it('can delete a strategy in the development environment', () => {
|
||||
cy.visit(`/projects/default/features/${featureToggleName}`);
|
||||
|
||||
cy.intercept(
|
||||
'DELETE',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
|
||||
req => {
|
||||
req.continue(res => {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
});
|
||||
}
|
||||
).as('deleteStrategy');
|
||||
|
||||
cy.get(
|
||||
'[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]'
|
||||
).click();
|
||||
cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').click();
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@deleteStrategy');
|
||||
});
|
||||
|
||||
it('can add a userId strategy to the development environment', () => {
|
||||
cy.visit(
|
||||
`/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId`
|
||||
);
|
||||
|
||||
if (ENTERPRISE) {
|
||||
cy.get('[data-testid=ADD_CONSTRAINT_ID]').click();
|
||||
cy.get('[data-testid=CONSTRAINT_AUTOCOMPLETE_ID]')
|
||||
.type('{downArrow}'.repeat(1))
|
||||
.type('{enter}');
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
}
|
||||
|
||||
cy.get('[data-testid=STRATEGY_INPUT_LIST]')
|
||||
.type('user1')
|
||||
.type('{enter}')
|
||||
.type('user2')
|
||||
.type('{enter}');
|
||||
cy.get('[data-testid=ADD_TO_STRATEGY_INPUT_LIST]').click();
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
|
||||
req => {
|
||||
expect(req.body.name).to.equal('userWithId');
|
||||
|
||||
expect(req.body.parameters.userIds.length).to.equal(11);
|
||||
|
||||
if (ENTERPRISE) {
|
||||
expect(req.body.constraints.length).to.equal(1);
|
||||
} else {
|
||||
expect(req.body.constraints.length).to.equal(0);
|
||||
}
|
||||
|
||||
req.continue(res => {
|
||||
strategyId = res.body.id;
|
||||
});
|
||||
}
|
||||
).as('addStrategyToFeature');
|
||||
|
||||
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
|
||||
cy.wait('@addStrategyToFeature');
|
||||
});
|
||||
|
||||
it('can add two variant to the feature', () => {
|
||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||
|
||||
cy.intercept(
|
||||
'PATCH',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||
req => {
|
||||
if (req.body.length === 1) {
|
||||
expect(req.body[0].op).to.equal('add');
|
||||
expect(req.body[0].path).to.match(/\//);
|
||||
expect(req.body[0].value.name).to.equal(variant1);
|
||||
} else if (req.body.length === 2) {
|
||||
expect(req.body[0].op).to.equal('replace');
|
||||
expect(req.body[0].path).to.match(/weight/);
|
||||
expect(req.body[0].value).to.equal(500);
|
||||
expect(req.body[1].op).to.equal('add');
|
||||
expect(req.body[1].path).to.match(/\//);
|
||||
expect(req.body[1].value.name).to.equal(variant2);
|
||||
}
|
||||
}
|
||||
).as('variantCreation');
|
||||
|
||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant1);
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@variantCreation');
|
||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant2);
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@variantCreation');
|
||||
});
|
||||
|
||||
it('can set weight to fixed value for one of the variants', () => {
|
||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||
|
||||
cy.get(`[data-testid=VARIANT_EDIT_BUTTON_${variant1}]`).click();
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid=VARIANT_NAME_INPUT]')
|
||||
.children()
|
||||
.find('input')
|
||||
.should('have.attr', 'disabled');
|
||||
cy.get('[data-testid=VARIANT_WEIGHT_CHECK]').find('input').check();
|
||||
cy.get('[data-testid=VARIANT_WEIGHT_INPUT]').clear().type('15');
|
||||
|
||||
cy.intercept(
|
||||
'PATCH',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||
req => {
|
||||
expect(req.body[0].op).to.equal('replace');
|
||||
expect(req.body[0].path).to.match(/weight/);
|
||||
expect(req.body[0].value).to.equal(850);
|
||||
expect(req.body[1].op).to.equal('replace');
|
||||
expect(req.body[1].path).to.match(/weightType/);
|
||||
expect(req.body[1].value).to.equal('fix');
|
||||
expect(req.body[2].op).to.equal('replace');
|
||||
expect(req.body[2].path).to.match(/weight/);
|
||||
expect(req.body[2].value).to.equal(150);
|
||||
}
|
||||
).as('variantUpdate');
|
||||
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@variantUpdate');
|
||||
cy.get(`[data-testid=VARIANT_WEIGHT_${variant1}]`).should(
|
||||
'have.text',
|
||||
'15 %'
|
||||
);
|
||||
});
|
||||
|
||||
it('can delete variant', () => {
|
||||
const variantName = 'to-be-deleted';
|
||||
|
||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variantName);
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
|
||||
cy.intercept(
|
||||
'PATCH',
|
||||
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||
req => {
|
||||
const patch = req.body.find(
|
||||
(patch: Record<string, string>) => patch.op === 'remove'
|
||||
);
|
||||
expect(patch.path).to.match(/\//);
|
||||
}
|
||||
).as('delete');
|
||||
|
||||
cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variantName}]`).click();
|
||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@delete');
|
||||
});
|
||||
});
|
113
frontend/cypress/integration/groups/groups.spec.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export {};
|
||||
const baseUrl = Cypress.config().baseUrl;
|
||||
const randomId = String(Math.random()).split('.')[1];
|
||||
const groupName = `unleash-e2e-${randomId}`;
|
||||
const userIds: any[] = [];
|
||||
|
||||
// Disable all active splash pages by visiting them.
|
||||
const disableActiveSplashScreens = () => {
|
||||
cy.visit(`/splash/operators`);
|
||||
};
|
||||
|
||||
describe('groups', () => {
|
||||
before(() => {
|
||||
disableActiveSplashScreens();
|
||||
cy.login();
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
cy.request('POST', `${baseUrl}/api/admin/user-admin`, {
|
||||
name: `unleash-e2e-user${i}-${randomId}`,
|
||||
email: `unleash-e2e-user${i}-${randomId}@test.com`,
|
||||
sendEmail: false,
|
||||
rootRole: 3,
|
||||
}).then(response => userIds.push(response.body.id));
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
userIds.forEach(id =>
|
||||
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`)
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/admin/groups');
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
});
|
||||
|
||||
it('can create a group', () => {
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
||||
|
||||
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
||||
cy.get("[data-testid='UG_DESC_ID']").type('hello-world');
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`).click();
|
||||
|
||||
cy.get("[data-testid='UG_CREATE_BTN_ID']").click();
|
||||
cy.wait('@createGroup');
|
||||
cy.contains(groupName);
|
||||
});
|
||||
|
||||
it('gives an error if a group exists with the same name', () => {
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
||||
|
||||
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
||||
cy.get("[data-testid='INPUT_ERROR_TEXT'").contains(
|
||||
'A group with that name already exists.'
|
||||
);
|
||||
});
|
||||
|
||||
it('can edit a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get("[data-testid='UG_EDIT_BTN_ID']").click();
|
||||
|
||||
cy.get("[data-testid='UG_DESC_ID']").type('-my edited description');
|
||||
|
||||
cy.get("[data-testid='UG_SAVE_BTN_ID']").click();
|
||||
|
||||
cy.contains('hello-world-my edited description');
|
||||
});
|
||||
|
||||
it('can add user to a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get("[data-testid='UG_EDIT_USERS_BTN_ID']").click();
|
||||
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`).click();
|
||||
|
||||
cy.get("[data-testid='UG_SAVE_BTN_ID']").click();
|
||||
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`);
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`);
|
||||
});
|
||||
|
||||
it('can remove user from a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get(`[data-testid='UG_REMOVE_USER_BTN_ID-${userIds[1]}']`).click();
|
||||
|
||||
cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click();
|
||||
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`);
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`).should('not.exist');
|
||||
});
|
||||
|
||||
it('can delete a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get("[data-testid='UG_DELETE_BTN_ID']").click();
|
||||
cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click();
|
||||
|
||||
cy.contains(groupName).should('not.exist');
|
||||
});
|
||||
});
|
@ -0,0 +1,147 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import {
|
||||
PA_ASSIGN_BUTTON_ID,
|
||||
PA_ASSIGN_CREATE_ID,
|
||||
PA_EDIT_BUTTON_ID,
|
||||
PA_REMOVE_BUTTON_ID,
|
||||
PA_ROLE_ID,
|
||||
PA_USERS_GROUPS_ID,
|
||||
PA_USERS_GROUPS_TITLE_ID,
|
||||
} from '../../../../src/utils/testIds';
|
||||
|
||||
export {};
|
||||
const baseUrl = Cypress.config().baseUrl;
|
||||
const randomId = String(Math.random()).split('.')[1];
|
||||
const groupAndProjectName = `group-e2e-${randomId}`;
|
||||
const userName = `user-e2e-${randomId}`;
|
||||
const groupIds: any[] = [];
|
||||
const userIds: any[] = [];
|
||||
|
||||
// Disable all active splash pages by visiting them.
|
||||
const disableActiveSplashScreens = () => {
|
||||
cy.visit(`/splash/operators`);
|
||||
};
|
||||
|
||||
describe('project-access', () => {
|
||||
before(() => {
|
||||
disableActiveSplashScreens();
|
||||
cy.login();
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const name = `${i}-${userName}`;
|
||||
cy.request('POST', `${baseUrl}/api/admin/user-admin`, {
|
||||
name: name,
|
||||
email: `${name}@test.com`,
|
||||
sendEmail: false,
|
||||
rootRole: 3,
|
||||
})
|
||||
.as(name)
|
||||
.then(response => {
|
||||
const id = response.body.id;
|
||||
userIds.push(id);
|
||||
cy.request('POST', `${baseUrl}/api/admin/groups`, {
|
||||
name: `${i}-${groupAndProjectName}`,
|
||||
users: [{ user: { id: id } }],
|
||||
}).then(response => {
|
||||
const id = response.body.id;
|
||||
groupIds.push(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
cy.request('POST', `${baseUrl}/api/admin/projects`, {
|
||||
id: groupAndProjectName,
|
||||
name: groupAndProjectName,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
userIds.forEach(id =>
|
||||
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`)
|
||||
);
|
||||
groupIds.forEach(id =>
|
||||
cy.request('DELETE', `${baseUrl}/api/admin/groups/${id}`)
|
||||
);
|
||||
|
||||
cy.request(
|
||||
'DELETE',
|
||||
`${baseUrl}/api/admin/projects/${groupAndProjectName}`
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit(`/projects/${groupAndProjectName}/access`);
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
});
|
||||
|
||||
it('can assign permissions to user', () => {
|
||||
cy.get(`[data-testid='${PA_ASSIGN_BUTTON_ID}']`).click();
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
||||
).as('assignAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||
cy.contains(`1-${userName}`).click();
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_TITLE_ID}']`).click();
|
||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||
cy.contains('full control over the project').click({ force: true });
|
||||
|
||||
cy.get(`[data-testid='${PA_ASSIGN_CREATE_ID}']`).click();
|
||||
cy.wait('@assignAccess');
|
||||
cy.contains(`1-${userName}`);
|
||||
});
|
||||
|
||||
it('can assign permissions to group', () => {
|
||||
cy.get(`[data-testid='${PA_ASSIGN_BUTTON_ID}']`).click();
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
||||
).as('assignAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||
cy.contains(`1-${groupAndProjectName}`).click({ force: true });
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_TITLE_ID}']`).click();
|
||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||
cy.contains('full control over the project').click({ force: true });
|
||||
|
||||
cy.get(`[data-testid='${PA_ASSIGN_CREATE_ID}']`).click();
|
||||
cy.wait('@assignAccess');
|
||||
cy.contains(`1-${groupAndProjectName}`);
|
||||
});
|
||||
|
||||
it('can edit role', () => {
|
||||
cy.get(`[data-testid='${PA_EDIT_BUTTON_ID}']`).first().click();
|
||||
|
||||
cy.intercept(
|
||||
'PUT',
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
||||
).as('editAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||
cy.contains('within a project are allowed').click({ force: true });
|
||||
|
||||
cy.get(`[data-testid='${PA_ASSIGN_CREATE_ID}']`).click();
|
||||
cy.wait('@editAccess');
|
||||
cy.get("td span:contains('Owner')").should('have.length', 2);
|
||||
cy.get("td span:contains('Member')").should('have.length', 1);
|
||||
});
|
||||
|
||||
it('can remove access', () => {
|
||||
cy.get(`[data-testid='${PA_REMOVE_BUTTON_ID}']`).first().click();
|
||||
|
||||
cy.intercept(
|
||||
'DELETE',
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
||||
).as('removeAccess');
|
||||
|
||||
cy.contains("Yes, I'm sure").click();
|
||||
|
||||
cy.wait('@removeAccess');
|
||||
cy.contains(`1-${groupAndProjectName} has been removed from project`);
|
||||
});
|
||||
});
|
57
frontend/cypress/integration/segments/segments.spec.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export {};
|
||||
const randomId = String(Math.random()).split('.')[1];
|
||||
const segmentName = `unleash-e2e-${randomId}`;
|
||||
|
||||
// Disable all active splash pages by visiting them.
|
||||
const disableActiveSplashScreens = () => {
|
||||
cy.visit(`/splash/operators`);
|
||||
};
|
||||
|
||||
describe('segments', () => {
|
||||
before(() => {
|
||||
disableActiveSplashScreens();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/segments');
|
||||
});
|
||||
|
||||
it('can create a segment', () => {
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/segments').as('createSegment');
|
||||
|
||||
cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName);
|
||||
cy.get("[data-testid='SEGMENT_DESC_ID']").type('hello-world');
|
||||
cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").click();
|
||||
cy.get("[data-testid='SEGMENT_CREATE_BTN_ID']").click();
|
||||
cy.wait('@createSegment');
|
||||
cy.contains(segmentName);
|
||||
});
|
||||
|
||||
it('gives an error if a segment exists with the same name', () => {
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click();
|
||||
|
||||
cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName);
|
||||
cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").should('be.disabled');
|
||||
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
|
||||
'Segment name already exists'
|
||||
);
|
||||
});
|
||||
|
||||
it('can delete a segment', () => {
|
||||
cy.get(`[data-testid='SEGMENT_DELETE_BTN_ID_${segmentName}']`).click();
|
||||
|
||||
cy.get("[data-testid='SEGMENT_DIALOG_NAME_ID']").type(segmentName);
|
||||
cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click();
|
||||
|
||||
cy.contains(segmentName).should('not.exist');
|
||||
});
|
||||
});
|
22
frontend/cypress/plugins/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
45
frontend/cypress/support/commands.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
const AUTH_USER = Cypress.env('AUTH_USER');
|
||||
const AUTH_PASSWORD = Cypress.env('AUTH_PASSWORD');
|
||||
|
||||
Cypress.Commands.add('login', (user = AUTH_USER, password = AUTH_PASSWORD) =>
|
||||
cy.session(user, () => {
|
||||
cy.visit('/');
|
||||
cy.wait(1000);
|
||||
cy.get("[data-testid='LOGIN_EMAIL_ID']").type(user);
|
||||
|
||||
if (AUTH_PASSWORD) {
|
||||
cy.get("[data-testid='LOGIN_PASSWORD_ID']").type(password);
|
||||
}
|
||||
|
||||
cy.get("[data-testid='LOGIN_BUTTON']").click();
|
||||
|
||||
// Wait for the login redirect to complete.
|
||||
cy.get("[data-testid='HEADER_USER_AVATAR']");
|
||||
})
|
||||
);
|
28
frontend/cypress/support/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(user?: string, password?: string): Chainable<null>;
|
||||
}
|
||||
}
|
||||
}
|
23
frontend/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="::faviconPrefix::/favicon.ico" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="baseUriPath" content="::baseUriPath::" />
|
||||
<meta name="cdnPrefix" content="::cdnPrefix::" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="unleash" />
|
||||
<title>Unleash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Sen:wght@400;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
9
frontend/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
require('pkginfo')(module, 'version');
|
||||
const path = require('path');
|
||||
|
||||
const { version } = module.exports;
|
||||
|
||||
module.exports = {
|
||||
publicFolder: path.join(__dirname, 'build'),
|
||||
version
|
||||
};
|
131
frontend/package.json
Normal file
@ -0,0 +1,131 @@
|
||||
{
|
||||
"name": "unleash-frontend-local",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"index.js",
|
||||
"build/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"start": "vite",
|
||||
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
|
||||
"start:enterprise": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
|
||||
"start:demo": "UNLEASH_BASE_PATH=/demo/ yarn start",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"fmt": "prettier src --write --loglevel warn",
|
||||
"fmt:check": "prettier src --check",
|
||||
"e2e": "yarn run cypress open --config baseUrl='http://localhost:3000' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all",
|
||||
"e2e:heroku": "yarn run cypress open --config baseUrl='http://localhost:3000' --env AUTH_USER=example@example.com",
|
||||
"prepare": "yarn run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-json": "6.0.0",
|
||||
"@emotion/react": "11.9.3",
|
||||
"@emotion/styled": "11.9.3",
|
||||
"@mui/icons-material": "5.8.4",
|
||||
"@mui/lab": "5.0.0-alpha.95",
|
||||
"@mui/material": "5.10.1",
|
||||
"@openapitools/openapi-generator-cli": "2.5.1",
|
||||
"@testing-library/dom": "8.17.1",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "7.0.2",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/debounce": "1.2.1",
|
||||
"@types/deep-diff": "1.0.1",
|
||||
"@types/jest": "28.1.7",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/node": "17.0.18",
|
||||
"@types/react": "17.0.48",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-table": "7.7.12",
|
||||
"@types/react-test-renderer": "17.0.2",
|
||||
"@types/react-timeago": "4.1.3",
|
||||
"@types/semver": "7.3.12",
|
||||
"@uiw/react-codemirror": "4.11.5",
|
||||
"@vitejs/plugin-react": "1.3.2",
|
||||
"chart.js": "3.9.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"classnames": "2.3.1",
|
||||
"copy-to-clipboard": "3.3.2",
|
||||
"cypress": "9.7.0",
|
||||
"date-fns": "2.29.2",
|
||||
"debounce": "1.2.1",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"fast-json-patch": "3.1.1",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"immer": "9.0.15",
|
||||
"jsdom": "20.0.0",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"msw": "0.45.0",
|
||||
"pkginfo": "0.4.1",
|
||||
"plausible-tracker": "0.3.8",
|
||||
"prettier": "2.7.1",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-chartjs-2": "4.3.1",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hooks-global-state": "2.0.0",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-table": "7.8.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"react-timeago": "7.1.0",
|
||||
"sass": "1.54.5",
|
||||
"semver": "7.3.7",
|
||||
"swr": "1.3.0",
|
||||
"tss-react": "3.7.1",
|
||||
"typescript": "4.7.4",
|
||||
"vite": "2.9.15",
|
||||
"vite-plugin-env-compatible": "1.1.1",
|
||||
"vite-plugin-svgr": "2.2.1",
|
||||
"vite-tsconfig-paths": "3.5.0",
|
||||
"vitest": "0.22.1",
|
||||
"whatwg-fetch": "3.6.2",
|
||||
"@uiw/codemirror-theme-duotone": "4.11.5"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
|
||||
"\\.svg": "<rootDir>/src/__mocks__/svgMock.js",
|
||||
"\\.(css|scss)$": "identity-obj-proxy"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"parserOptions": {
|
||||
"warnOnUnsupportedTypeScriptVersion": false
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-globals": "off",
|
||||
"no-useless-computed-key": "off",
|
||||
"import/no-anonymous-default-export": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"cypress"
|
||||
]
|
||||
}
|
||||
}
|
BIN
frontend/public/cs_CZ.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/public/da-DK.png
Normal file
After Width: | Height: | Size: 645 B |
1
frontend/public/datadog.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="2500" viewBox=".27 .27 800.01 858.98" width="2328" xmlns="http://www.w3.org/2000/svg"><path d="m670.38 608.27-71.24-46.99-59.43 99.27-69.12-20.21-60.86 92.89 3.12 29.24 330.9-60.97-19.22-206.75zm-308.59-89.14 53.09-7.3c8.59 3.86 14.57 5.33 24.87 7.95 16.04 4.18 34.61 8.19 62.11-5.67 6.4-3.17 19.73-15.36 25.12-22.31l217.52-39.46 22.19 268.56-372.65 67.16zm404.06-96.77-21.47 4.09-41.25-426.18-702.86 81.5 86.59 702.68 82.27-11.94c-6.57-9.38-16.8-20.73-34.27-35.26-24.23-20.13-15.66-54.32-1.37-75.91 18.91-36.48 116.34-82.84 110.82-141.15-1.98-21.2-5.35-48.8-25.03-67.71-.74 7.85.59 15.41.59 15.41s-8.08-10.31-12.11-24.37c-4-5.39-7.14-7.11-11.39-14.31-3.03 8.33-2.63 17.99-2.63 17.99s-6.61-15.62-7.68-28.8c-3.92 5.9-4.91 17.11-4.91 17.11s-8.59-24.62-6.63-37.88c-3.92-11.54-15.54-34.44-12.25-86.49 21.45 15.03 68.67 11.46 87.07-15.66 6.11-8.98 10.29-33.5-3.05-81.81-8.57-30.98-29.79-77.11-38.06-94.61l-.99.71c4.36 14.1 13.35 43.66 16.8 57.99 10.44 43.47 13.24 58.6 8.34 78.64-4.17 17.42-14.17 28.82-39.52 41.56-25.35 12.78-58.99-18.32-61.12-20.04-24.63-19.62-43.68-51.63-45.81-67.18-2.21-17.02 9.81-27.24 15.87-41.16-8.67 2.48-18.34 6.88-18.34 6.88s11.54-11.94 25.77-22.27c5.89-3.9 9.35-6.38 15.56-11.54-8.99-.15-16.29.11-16.29.11s14.99-8.1 30.53-14c-11.37-.5-22.25-.08-22.25-.08s33.45-14.96 59.87-25.94c18.17-7.45 35.92-5.25 45.89 9.17 13.09 18.89 26.84 29.15 55.98 35.51 17.89-7.93 23.33-12.01 45.81-18.13 19.79-21.76 35.33-24.58 35.33-24.58s-7.71 7.07-9.77 18.18c11.22-8.84 23.52-16.22 23.52-16.22s-4.76 5.88-9.2 15.22l1.03 1.53c13.09-7.85 28.48-14.04 28.48-14.04s-4.4 5.56-9.56 12.76c9.87-.08 29.89.42 37.66 1.3 45.87 1.01 55.39-48.99 72.99-55.26 22.04-7.87 31.89-12.63 69.45 24.26 32.23 31.67 57.41 88.36 44.91 101.06-10.48 10.54-31.16-4.11-54.08-32.68-12.11-15.13-21.27-33.01-25.56-55.74-3.62-19.18-17.71-30.31-17.71-30.31s8.18 18.18 8.18 34.24c0 8.77 1.1 41.56 15.16 59.96-1.39 2.69-2.04 13.31-3.58 15.34-16.36-19.77-51.49-33.92-57.22-38.09 19.39 15.89 63.96 52.39 81.08 87.37 16.19 33.08 6.65 63.4 14.84 71.25 2.33 2.25 34.82 42.73 41.07 63.07 10.9 35.45.65 72.7-13.62 95.81l-39.85 6.21c-5.83-1.62-9.76-2.43-14.99-5.46 2.88-5.1 8.61-17.82 8.67-20.44l-2.25-3.95c-12.4 17.57-33.18 34.63-50.44 44.43-22.59 12.8-48.63 10.83-65.58 5.58-48.11-14.84-93.6-47.35-104.57-55.89 0 0-.34 6.82 1.73 8.35 12.13 13.68 39.92 38.43 66.78 55.68l-57.26 6.3 27.07 210.78c-12 1.72-13.87 2.56-27.01 4.43-11.58-40.91-33.73-67.62-57.94-83.18-21.35-13.72-50.8-16.81-78.99-11.23l-1.81 2.1c19.6-2.04 42.74.8 66.51 15.85 23.33 14.75 42.13 52.85 49.05 75.79 8.86 29.32 14.99 60.68-8.86 93.92-16.97 23.63-66.51 36.69-106.53 8.44 10.69 17.19 25.14 31.25 44.59 33.9 28.88 3.92 56.29-1.09 75.16-20.46 16.11-16.56 24.65-51.19 22.4-87.66l25.49-3.7 9.2 65.46 421.98-50.81zm-256.73-177.77c-1.18 2.69-3.03 4.45-.25 13.2l.17.5.44 1.13 1.16 2.62c5.01 10.24 10.51 19.9 19.7 24.83 2.38-.4 4.84-.67 7.39-.8 8.63-.38 14.08.99 17.54 2.85.31-1.72.38-4.24.19-7.95-.67-12.97 2.57-35.03-22.36-46.64-9.41-4.37-22.61-3.02-27.01 2.43.8.1 1.52.27 2.08.46 6.65 2.33 2.14 4.62.95 7.37m69.87 121.02c-3.27-1.8-18.55-1.09-29.29.19-20.46 2.41-42.55 9.51-47.39 13.29-8.8 6.8-4.8 18.66 1.7 23.53 18.23 13.62 34.21 22.75 51.08 20.53 10.36-1.36 19.49-17.76 25.96-32.64 4.43-10.25 4.43-21.31-2.06-24.9m-181.14-104.96c5.77-5.48-28.74-12.68-55.52 5.58-19.75 13.47-20.38 42.35-1.47 58.72 1.89 1.62 3.45 2.77 4.91 3.71 5.52-2.6 11.81-5.23 19.05-7.58 12.23-3.97 22.4-6.02 30.76-7.11 4-4.47 8.65-12.34 7.49-26.59-1.58-19.33-16.23-16.26-5.22-26.73" fill="#632ca6"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
BIN
frontend/public/de_DE.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
frontend/public/en-GB.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/en-IN.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
frontend/public/en-US.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/favicon_old.ico
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/flags-normal/ad.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/flags-normal/ae.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
frontend/public/flags-normal/af.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
frontend/public/flags-normal/ag.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
frontend/public/flags-normal/al.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/public/flags-normal/am.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
frontend/public/flags-normal/ao.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
frontend/public/flags-normal/ar.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/flags-normal/at.png
Normal file
After Width: | Height: | Size: 225 B |
BIN
frontend/public/flags-normal/au.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/public/flags-normal/az.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/public/flags-normal/ba.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/public/flags-normal/bb.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/public/flags-normal/bd.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
frontend/public/flags-normal/be.png
Normal file
After Width: | Height: | Size: 683 B |
BIN
frontend/public/flags-normal/bf.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/public/flags-normal/bg.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
frontend/public/flags-normal/bh.png
Normal file
After Width: | Height: | Size: 879 B |
BIN
frontend/public/flags-normal/bi.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
frontend/public/flags-normal/bj.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
frontend/public/flags-normal/bn.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/flags-normal/bo.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
frontend/public/flags-normal/br.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/flags-normal/bs.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/public/flags-normal/bt.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
frontend/public/flags-normal/bw.png
Normal file
After Width: | Height: | Size: 523 B |
BIN
frontend/public/flags-normal/by.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
frontend/public/flags-normal/bz.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
frontend/public/flags-normal/ca.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/public/flags-normal/cd.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/flags-normal/cf.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/public/flags-normal/cg.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/public/flags-normal/ch.png
Normal file
After Width: | Height: | Size: 370 B |
BIN
frontend/public/flags-normal/ci.png
Normal file
After Width: | Height: | Size: 540 B |
BIN
frontend/public/flags-normal/cl.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/public/flags-normal/cm.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/public/flags-normal/cn.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/public/flags-normal/co.png
Normal file
After Width: | Height: | Size: 560 B |
BIN
frontend/public/flags-normal/cr.png
Normal file
After Width: | Height: | Size: 261 B |
BIN
frontend/public/flags-normal/cu.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/public/flags-normal/cv.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
frontend/public/flags-normal/cy.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
frontend/public/flags-normal/cz.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/public/flags-normal/de.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
frontend/public/flags-normal/dj.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/flags-normal/dk.png
Normal file
After Width: | Height: | Size: 645 B |
BIN
frontend/public/flags-normal/dm.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/flags-normal/do.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/flags-normal/dz.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
frontend/public/flags-normal/ec.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
frontend/public/flags-normal/ee.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
frontend/public/flags-normal/eg.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/flags-normal/eh.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
frontend/public/flags-normal/er.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/flags-normal/es.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/flags-normal/et.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
frontend/public/flags-normal/fi.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
frontend/public/flags-normal/fj.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/flags-normal/fm.png
Normal file
After Width: | Height: | Size: 2.2 KiB |