1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

Merge branch 'main' into feat/search-toggles-project

This commit is contained in:
Youssef Khedher 2022-03-01 09:35:46 +01:00 committed by GitHub
commit 57268fb083
156 changed files with 1016 additions and 1102 deletions

View File

@ -17,9 +17,9 @@ jobs:
- name: Run Cypress - name: Run Cypress
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development" env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }}
config: baseUrl=${{ github.event.deployment_status.target_url }} config: baseUrl=${{ github.event.deployment_status.target_url }}
record: true record: true
spec: cypress/integration/auth/auth.spec.js spec: cypress/integration/auth/auth.spec.ts
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View File

@ -17,9 +17,9 @@ jobs:
- name: Run Cypress - name: Run Cypress
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development" env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }}
config: baseUrl=${{ github.event.deployment_status.target_url }} config: baseUrl=${{ github.event.deployment_status.target_url }}
record: true record: true
spec: cypress/integration/feature-toggle/feature.spec.js spec: cypress/integration/feature/feature.spec.ts
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View File

@ -14,7 +14,7 @@ jobs:
node-version: [14.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:

View File

@ -1,4 +1,6 @@
{ {
"projectId": "tc2qff", "projectId": "tc2qff",
"defaultCommandTimeout": 12000 "defaultCommandTimeout": 12000,
"screenshotOnRunFailure": false,
"video": false
} }

View File

@ -1,15 +1,4 @@
/* eslint-disable jest/no-conditional-expect */
/// <reference types="cypress" /> /// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
const username = 'test@test.com'; const username = 'test@test.com';
const password = 'qY70$NDcJNXA'; const password = 'qY70$NDcJNXA';

View File

@ -1,82 +1,48 @@
/* eslint-disable jest/no-conditional-expect */
/// <reference types="cypress" /> /// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
let featureToggleName = ''; import { disableFeatureStrategiesProductionGuard } from '../../../src/component/feature/FeatureView/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesProductionGuard/FeatureStrategiesProductionGuard';
let enterprise = false;
let strategyId = '';
let defaultEnv = 'development';
describe('feature toggle', () => { const randomId = String(Math.random()).split('.')[1];
before(() => { const featureToggleName = `unleash-e2e-${randomId}`;
featureToggleName = `unleash-e2e-${Math.floor(Math.random() * 100)}`; const enterprise = Boolean(Cypress.env('ENTERPRISE'));
enterprise = Boolean(Cypress.env('ENTERPRISE')); const passwordAuth = Cypress.env('PASSWORD_AUTH');
const env = Cypress.env('DEFAULT_ENV');
if (env) {
defaultEnv = env;
}
});
after(() => {
const authToken = Cypress.env('AUTH_TOKEN'); const authToken = Cypress.env('AUTH_TOKEN');
const baseUrl = Cypress.config().baseUrl;
let strategyId = '';
describe('feature', () => {
after(() => {
cy.request({ cy.request({
method: 'DELETE', method: 'DELETE',
url: `${ url: `${baseUrl}/api/admin/features/${featureToggleName}`,
Cypress.config().baseUrl headers: { Authorization: authToken },
}/api/admin/features/${featureToggleName}`,
headers: {
Authorization: authToken,
},
}); });
cy.request({ cy.request({
method: 'DELETE', method: 'DELETE',
url: `${ url: `${baseUrl}/api/admin/archive/${featureToggleName}`,
Cypress.config().baseUrl headers: { Authorization: authToken },
}/api/admin/archive/${featureToggleName}`,
headers: {
Authorization: authToken,
},
}); });
}); });
beforeEach(() => { beforeEach(() => {
// Cypress starts out with a blank slate for each test disableFeatureStrategiesProductionGuard();
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
const passwordAuth = Cypress.env('PASSWORD_AUTH');
enterprise = Boolean(Cypress.env('ENTERPRISE'));
cy.visit('/'); cy.visit('/');
if (passwordAuth) { if (passwordAuth) {
cy.get('[data-test="LOGIN_EMAIL_ID"]').type('test@test.com'); cy.get('[data-test="LOGIN_EMAIL_ID"]').type('test@test.com');
cy.get('[data-test="LOGIN_PASSWORD_ID"]').type('qY70$NDcJNXA'); cy.get('[data-test="LOGIN_PASSWORD_ID"]').type('qY70$NDcJNXA');
cy.get("[data-test='LOGIN_BUTTON']").click(); cy.get("[data-test='LOGIN_BUTTON']").click();
} else { } else {
cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com'); cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com');
cy.get('[data-test=LOGIN_BUTTON]').click(); cy.get('[data-test=LOGIN_BUTTON]').click();
} }
// Wait for the login redirects to complete.
cy.get('[data-test=HEADER_USER_AVATAR');
}); });
it('Creates a feature toggle', () => { it('can create a feature toggle', () => {
if ( if (document.querySelector("[data-test='CLOSE_SPLASH']")) {
document.querySelectorAll("[data-test='CLOSE_SPLASH']").length > 0
) {
cy.get("[data-test='CLOSE_SPLASH']").click(); cy.get("[data-test='CLOSE_SPLASH']").click();
} }
@ -87,14 +53,13 @@ describe('feature toggle', () => {
); );
cy.get("[data-test='CF_NAME_ID'").type(featureToggleName); cy.get("[data-test='CF_NAME_ID'").type(featureToggleName);
cy.get("[data-test='CF_DESC_ID'").type('hellowrdada'); cy.get("[data-test='CF_DESC_ID'").type('hello-world');
cy.get("[data-test='CF_CREATE_BTN_ID']").click(); cy.get("[data-test='CF_CREATE_BTN_ID']").click();
cy.wait('@createFeature'); cy.wait('@createFeature');
cy.url().should('include', featureToggleName); cy.url().should('include', featureToggleName);
}); });
it('Gives an error if a toggle exists with the same name', () => { it('gives an error if a toggle exists with the same name', () => {
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click(); cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
cy.intercept('POST', '/api/admin/projects/default/features').as( cy.intercept('POST', '/api/admin/projects/default/features').as(
@ -102,16 +67,14 @@ describe('feature toggle', () => {
); );
cy.get("[data-test='CF_NAME_ID'").type(featureToggleName); cy.get("[data-test='CF_NAME_ID'").type(featureToggleName);
cy.get("[data-test='CF_DESC_ID'").type('hellowrdada'); cy.get("[data-test='CF_DESC_ID'").type('hello-world');
cy.get("[data-test='CF_CREATE_BTN_ID']").click(); cy.get("[data-test='CF_CREATE_BTN_ID']").click();
cy.get("[data-test='INPUT_ERROR_TEXT']").contains( cy.get("[data-test='INPUT_ERROR_TEXT']").contains(
'A feature with this name already exists' 'A feature with this name already exists'
); );
}); });
it('Gives an error if a toggle name is url unsafe', () => { it('gives an error if a toggle name is url unsafe', () => {
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click(); cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
cy.intercept('POST', '/api/admin/projects/default/features').as( cy.intercept('POST', '/api/admin/projects/default/features').as(
@ -119,17 +82,14 @@ describe('feature toggle', () => {
); );
cy.get("[data-test='CF_NAME_ID'").type('featureToggleUnsafe####$#//'); cy.get("[data-test='CF_NAME_ID'").type('featureToggleUnsafe####$#//');
cy.get("[data-test='CF_DESC_ID'").type('hellowrdada'); cy.get("[data-test='CF_DESC_ID'").type('hello-world');
cy.get("[data-test='CF_CREATE_BTN_ID']").click(); cy.get("[data-test='CF_CREATE_BTN_ID']").click();
cy.get("[data-test='INPUT_ERROR_TEXT']").contains( cy.get("[data-test='INPUT_ERROR_TEXT']").contains(
`"name" must be URL friendly` `"name" must be URL friendly`
); );
}); });
it('Can add a gradual rollout strategy to the development environment', () => { it('can add a gradual rollout strategy to the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`); cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click(); cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click(); cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click();
@ -147,10 +107,9 @@ describe('feature toggle', () => {
cy.intercept( cy.intercept(
'POST', 'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`, `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
req => { req => {
expect(req.body.name).to.equal('flexibleRollout'); expect(req.body.name).to.equal('flexibleRollout');
expect(req.body.parameters.groupId).to.equal(featureToggleName); expect(req.body.parameters.groupId).to.equal(featureToggleName);
expect(req.body.parameters.stickiness).to.equal('default'); expect(req.body.parameters.stickiness).to.equal('default');
expect(req.body.parameters.rollout).to.equal(30); expect(req.body.parameters.rollout).to.equal(30);
@ -172,7 +131,6 @@ describe('feature toggle', () => {
}); });
it('can update a strategy in the development environment', () => { it('can update a strategy in the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`); cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click(); cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click();
@ -188,7 +146,6 @@ describe('feature toggle', () => {
.first() .first()
.click(); .click();
let newGroupId = 'new-group-id';
cy.get('[data-test=FLEXIBLE_STRATEGY_GROUP_ID]') cy.get('[data-test=FLEXIBLE_STRATEGY_GROUP_ID]')
.first() .first()
.clear() .clear()
@ -196,9 +153,9 @@ describe('feature toggle', () => {
cy.intercept( cy.intercept(
'PUT', 'PUT',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`, `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
req => { req => {
expect(req.body.parameters.groupId).to.equal(newGroupId); expect(req.body.parameters.groupId).to.equal('new-group-id');
expect(req.body.parameters.stickiness).to.equal('sessionId'); expect(req.body.parameters.stickiness).to.equal('sessionId');
expect(req.body.parameters.rollout).to.equal(60); expect(req.body.parameters.rollout).to.equal(60);
@ -219,12 +176,11 @@ describe('feature toggle', () => {
}); });
it('can delete a strategy in the development environment', () => { it('can delete a strategy in the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`); cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.intercept( cy.intercept(
'DELETE', 'DELETE',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`, `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
req => { req => {
req.continue(res => { req.continue(res => {
expect(res.statusCode).to.equal(200); expect(res.statusCode).to.equal(200);
@ -237,8 +193,7 @@ describe('feature toggle', () => {
cy.wait('@deleteStrategy'); cy.wait('@deleteStrategy');
}); });
it('Can add a userid strategy to the development environment', () => { it('can add a userid strategy to the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`); cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click(); cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-3').click(); cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-3').click();
@ -260,7 +215,7 @@ describe('feature toggle', () => {
cy.intercept( cy.intercept(
'POST', 'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`, `/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
req => { req => {
expect(req.body.name).to.equal('userWithId'); expect(req.body.name).to.equal('userWithId');
@ -282,11 +237,12 @@ describe('feature toggle', () => {
cy.wait('@addStrategyToFeature'); cy.wait('@addStrategyToFeature');
}); });
it('Can add two variant to the feature', () => { it('can add two variant to the feature', () => {
const variantName = 'my-new-variant'; const variantName = 'my-new-variant';
const secondVariantName = 'my-second-variant'; const secondVariantName = 'my-second-variant';
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/variants`); cy.visit(`/projects/default/features/${featureToggleName}/variants`);
cy.intercept( cy.intercept(
'PATCH', 'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/variants`, `/api/admin/projects/default/features/${featureToggleName}/variants`,
@ -304,20 +260,23 @@ describe('feature toggle', () => {
expect(req.body[1].value.name).to.equal(secondVariantName); expect(req.body[1].value.name).to.equal(secondVariantName);
} }
} }
).as('variantcreation'); ).as('variantCreation');
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
cy.get('[data-test=VARIANT_NAME_INPUT]').type(variantName);
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@variantcreation');
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
cy.get('[data-test=VARIANT_NAME_INPUT]').type(secondVariantName);
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@variantcreation');
});
it('Can set weight to fixed value for one of the variants', () => {
cy.wait(1000);
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
cy.get('[data-test=VARIANT_NAME_INPUT]').click().type(variantName);
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@variantCreation');
cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
cy.get('[data-test=VARIANT_NAME_INPUT]')
.click()
.type(secondVariantName);
cy.get('[data-test=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.visit(`/projects/default/features/${featureToggleName}/variants`);
cy.get('[data-test=VARIANT_EDIT_BUTTON]').first().click(); cy.get('[data-test=VARIANT_EDIT_BUTTON]').first().click();
cy.get('[data-test=VARIANT_NAME_INPUT]') cy.get('[data-test=VARIANT_NAME_INPUT]')
.children() .children()
@ -328,6 +287,7 @@ describe('feature toggle', () => {
.find('input') .find('input')
.check(); .check();
cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15'); cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15');
cy.intercept( cy.intercept(
'PATCH', 'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/variants`, `/api/admin/projects/default/features/${featureToggleName}/variants`,
@ -342,21 +302,23 @@ describe('feature toggle', () => {
expect(req.body[2].path).to.match(/weight/); expect(req.body[2].path).to.match(/weight/);
expect(req.body[2].value).to.equal(150); expect(req.body[2].value).to.equal(150);
} }
).as('variantupdate'); ).as('variantUpdate');
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click(); cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@variantupdate'); cy.wait('@variantUpdate');
cy.get('[data-test=VARIANT_WEIGHT]') cy.get('[data-test=VARIANT_WEIGHT]')
.first() .first()
.should('have.text', '15 %'); .should('have.text', '15 %');
}); });
it(`can delete variant`, () => { it('can delete variant', () => {
const variantName = 'to-be-deleted'; const variantName = 'to-be-deleted';
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/variants`); cy.visit(`/projects/default/features/${featureToggleName}/variants`);
cy.get('[data-test=ADD_VARIANT_BUTTON]').click(); cy.get('[data-test=ADD_VARIANT_BUTTON]').click();
cy.get('[data-test=VARIANT_NAME_INPUT]').type(variantName); cy.get('[data-test=VARIANT_NAME_INPUT]').click().type(variantName);
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click(); cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.intercept( cy.intercept(
'PATCH', 'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/variants`, `/api/admin/projects/default/features/${featureToggleName}/variants`,
@ -365,6 +327,7 @@ describe('feature toggle', () => {
expect(e.path).to.match(/\//); expect(e.path).to.match(/\//);
} }
).as('delete'); ).as('delete');
cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click(); cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click(); cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@delete'); cy.wait('@delete');

View File

@ -42,6 +42,7 @@
"@material-ui/core": "4.12.3", "@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2", "@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.60", "@material-ui/lab": "4.0.0-alpha.60",
"@testing-library/dom": "8.11.3",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.3", "@testing-library/react": "12.1.3",
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
@ -49,10 +50,9 @@
"@types/deep-diff": "1.0.1", "@types/deep-diff": "1.0.1",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",
"@types/lodash.clonedeep": "4.5.6", "@types/lodash.clonedeep": "4.5.6",
"@types/node": "14.18.12", "@types/node": "17.0.18",
"@types/react": "17.0.39", "@types/react": "17.0.39",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"@types/react-outside-click-handler": "1.3.1",
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-test-renderer": "17.0.1", "@types/react-test-renderer": "17.0.1",
"@types/react-timeago": "4.1.3", "@types/react-timeago": "4.1.3",
@ -63,7 +63,7 @@
"copy-to-clipboard": "3.3.1", "copy-to-clipboard": "3.3.1",
"craco": "0.0.3", "craco": "0.0.3",
"css-loader": "6.6.0", "css-loader": "6.6.0",
"cypress": "8.7.0", "cypress": "9.5.1",
"date-fns": "2.28.0", "date-fns": "2.28.0",
"debounce": "1.2.1", "debounce": "1.2.1",
"deep-diff": "1.0.2", "deep-diff": "1.0.2",
@ -79,14 +79,13 @@
"react-dnd-html5-backend": "14.1.0", "react-dnd-html5-backend": "14.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-hooks-global-state": "1.0.2", "react-hooks-global-state": "1.0.2",
"react-outside-click-handler": "1.3.0",
"react-router-dom": "5.3.0", "react-router-dom": "5.3.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-test-renderer": "16.14.0", "react-test-renderer": "17.0.2",
"react-timeago": "6.2.1", "react-timeago": "6.2.1",
"sass": "1.49.8", "sass": "1.49.9",
"swr": "1.2.2", "swr": "1.2.2",
"typescript": "4.5.5", "typescript": "4.6.2",
"web-vitals": "2.1.4" "web-vitals": "2.1.4"
}, },
"jest": { "jest": {
@ -117,6 +116,9 @@
"no-restricted-globals": "off", "no-restricted-globals": "off",
"no-useless-computed-key": "off", "no-useless-computed-key": "off",
"import/no-anonymous-default-export": "off" "import/no-anonymous-default-export": "off"
} },
"ignorePatterns": [
"cypress"
]
} }
} }

View File

@ -7,7 +7,9 @@ import { IAddonProvider } from '../../../../interfaces/addons';
interface IAddonProps { interface IAddonProps {
provider: IAddonProvider; provider: IAddonProvider;
checkedEvents: string[]; checkedEvents: string[];
setEventValue: (name: string) => void; setEventValue: (
name: string
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
error: Record<string, string>; error: Record<string, string>;
} }

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextField, FormControlLabel, Switch } from '@material-ui/core'; import { TextField, FormControlLabel, Switch, Button } from '@material-ui/core';
import { FormButtons, styles as commonStyles } from '../../common'; import { styles as commonStyles } from '../../common';
import { trim } from '../../common/util'; import { trim } from '../../common/util';
import { AddonParameters } from './AddonParameters/AddonParameters'; import { AddonParameters } from './AddonParameters/AddonParameters';
import { AddonEvents } from './AddonEvents/AddonEvents'; import { AddonEvents } from './AddonEvents/AddonEvents';
@ -79,7 +79,7 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
setErrors({ ...errors, events: undefined }); setErrors({ ...errors, events: undefined });
}; };
const handleCancel = () => { const onCancel = () => {
history.goBack(); history.goBack();
}; };
@ -203,10 +203,12 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
/> />
</section> </section>
<section className={styles.formSection}> <section className={styles.formSection}>
<FormButtons <Button type="submit" color="primary" variant="contained">
submitText={submitText} {submitText}
onCancel={handleCancel} </Button>
/> <Button type="button" onClick={onCancel}>
Cancel
</Button>
</section> </section>
</form> </form>
</PageContent> </PageContent>

View File

@ -1,4 +1,4 @@
import { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons'; import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
import { AvailableAddons } from './AvailableAddons/AvailableAddons'; import { AvailableAddons } from './AvailableAddons/AvailableAddons';
import { Avatar } from '@material-ui/core'; import { Avatar } from '@material-ui/core';
@ -12,7 +12,7 @@ import dataDogIcon from '../../../assets/icons/datadog.svg';
import { formatAssetPath } from '../../../utils/format-path'; import { formatAssetPath } from '../../../utils/format-path';
import useAddons from '../../../hooks/api/getters/useAddons/useAddons'; import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
const style = { const style: React.CSSProperties = {
width: '40px', width: '40px',
height: '40px', height: '40px',
marginRight: '16px', marginRight: '16px',

View File

@ -44,7 +44,6 @@ export const AvailableAddons = ({
onClick={() => onClick={() =>
history.push(`/addons/create/${provider.name}`) history.push(`/addons/create/${provider.name}`)
} }
tooltip={`Configure ${provider.name} Addon`}
> >
Configure Configure
</PermissionButton> </PermissionButton>

View File

@ -21,6 +21,7 @@ import AccessContext from '../../../../contexts/AccessContext';
import { IAddon } from '../../../../interfaces/addons'; import { IAddon } from '../../../../interfaces/addons';
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
import Dialogue from '../../../common/Dialogue'; import Dialogue from '../../../common/Dialogue';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
interface IConfigureAddonsProps { interface IConfigureAddonsProps {
getAddonIcon: (name: string) => ReactElement; getAddonIcon: (name: string) => ReactElement;
@ -59,8 +60,8 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
title: 'Success', title: 'Success',
text: 'Addon state switched successfully', text: 'Addon state switched successfully',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -122,7 +123,6 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
</PermissionIconButton> </PermissionIconButton>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_ADDON} permission={UPDATE_ADDON}
tooltip={'Edit Addon'}
onClick={() => { onClick={() => {
history.push(`/addons/edit/${addon.id}`); history.push(`/addons/edit/${addon.id}`);
}} }}
@ -131,7 +131,6 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
</PermissionIconButton> </PermissionIconButton>
<PermissionIconButton <PermissionIconButton
permission={DELETE_ADDON} permission={DELETE_ADDON}
tooltip={'Remove Addon'}
onClick={() => { onClick={() => {
setDeletedAddon(addon); setDeletedAddon(addon);
setShowDelete(true); setShowDelete(true);

View File

@ -24,7 +24,6 @@ import {
DELETE_API_TOKEN, DELETE_API_TOKEN,
} from '../../../providers/AccessProvider/permissions'; } from '../../../providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenList.styles'; import { useStyles } from './ApiTokenList.styles';
import { formatDateWithLocale } from '../../../common/util';
import Secret from './secret'; import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons'; import { Delete, FileCopy } from '@material-ui/icons';
import Dialogue from '../../../common/Dialogue'; import Dialogue from '../../../common/Dialogue';
@ -32,6 +31,7 @@ import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { useLocationSettings } from '../../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../../hooks/useLocationSettings';
import { formatDateYMD } from '../../../../utils/format-date';
interface IApiToken { interface IApiToken {
createdAt: Date; createdAt: Date;
@ -146,7 +146,7 @@ export const ApiTokenList = () => {
align="left" align="left"
className={styles.hideSM} className={styles.hideSM}
> >
{formatDateWithLocale( {formatDateYMD(
item.createdAt, item.createdAt,
locationSettings.locale locationSettings.locale
)} )}
@ -249,7 +249,7 @@ export const ApiTokenList = () => {
} }
data-test={CREATE_API_TOKEN_BUTTON} data-test={CREATE_API_TOKEN_BUTTON}
> >
Create API token New API token
</Button> </Button>
} }
/> />

View File

@ -1,15 +1,16 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import ApiTokenForm from '../ApiTokenForm/ApiTokenForm'; import ApiTokenForm from '../ApiTokenForm/ApiTokenForm';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import useApiTokenForm from '../hooks/useApiTokenForm'; import useApiTokenForm from '../hooks/useApiTokenForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useToast from '../../../../hooks/useToast';
import useApiTokensApi from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { ConfirmToken } from '../ConfirmToken/ConfirmToken'; import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { useState } from 'react'; import { useState } from 'react';
import { scrollToTop } from '../../../common/util'; import { scrollToTop } from '../../../common/util';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
export const CreateApiToken = () => { export const CreateApiToken = () => {
const { setToastApiError } = useToast(); const { setToastApiError } = useToast();
@ -49,8 +50,8 @@ export const CreateApiToken = () => {
setToken(api.secret); setToken(api.secret);
setShowConfirm(true); setShowConfirm(true);
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -95,9 +96,7 @@ export const CreateApiToken = () => {
mode="Create" mode="Create"
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton permission={ADMIN} type="submit"> <CreateButton name="token" permission={ADMIN} />
Create token
</PermissionButton>
</ApiTokenForm> </ApiTokenForm>
<ConfirmToken <ConfirmToken
open={showConfirm} open={showConfirm}

View File

@ -68,8 +68,8 @@ export const GoogleAuth = () => {
title: 'Settings stored', title: 'Settings stored',
type: 'success', type: 'success',
}); });
} catch (err) { } catch (error: unknown) {
setToastApiError(formatUnknownError(err)); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -79,8 +79,8 @@ export const OidcAuth = () => {
title: 'Settings stored', title: 'Settings stored',
type: 'success', type: 'success',
}); });
} catch (err) { } catch (error: unknown) {
setToastApiError(formatUnknownError(err)); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -51,8 +51,8 @@ export const PasswordAuth = () => {
type: 'success', type: 'success',
show: true, show: true,
}); });
} catch (err) { } catch (error: unknown) {
setToastApiError(formatUnknownError(err)); setToastApiError(formatUnknownError(error));
setDisablePasswordAuth(config.disabled); setDisablePasswordAuth(config.disabled);
} }
}; };

View File

@ -75,8 +75,8 @@ export const SamlAuth = () => {
title: 'Settings stored', title: 'Settings stored',
type: 'success', type: 'success',
}); });
} catch (err) { } catch (error: unknown) {
setToastApiError(formatUnknownError(err)); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -8,7 +8,6 @@ import {
Button, Button,
} from '@material-ui/core'; } from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew'; import OpenInNew from '@material-ui/icons/OpenInNew';
import { formatDateWithLocale } from '../../common/util';
import PageContent from '../../common/PageContent'; import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
@ -16,6 +15,7 @@ import { formatApiPath } from '../../../utils/format-path';
import useInvoices from '../../../hooks/api/getters/useInvoices/useInvoices'; import useInvoices from '../../../hooks/api/getters/useInvoices/useInvoices';
import { IInvoice } from '../../../interfaces/invoice'; import { IInvoice } from '../../../interfaces/invoice';
import { useLocationSettings } from '../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../hooks/useLocationSettings';
import { formatDateYMD } from '../../../utils/format-date';
const PORTAL_URL = formatApiPath('api/admin/invoices/portal'); const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
@ -87,7 +87,7 @@ const InvoiceList = () => {
style={{ textAlign: 'left' }} style={{ textAlign: 'left' }}
> >
{item.dueDate && {item.dueDate &&
formatDateWithLocale( formatDateYMD(
item.dueDate, item.dueDate,
locationSettings.locale locationSettings.locale
)} )}

View File

@ -1,12 +1,13 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useProjectRolesApi from '../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi'; import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
import useProjectRoleForm from '../hooks/useProjectRoleForm'; import useProjectRoleForm from '../hooks/useProjectRoleForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast'; import useToast from 'hooks/useToast';
import PermissionButton from '../../../common/PermissionButton/PermissionButton'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/format-unknown-error';
const CreateProjectRole = () => { const CreateProjectRole = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -49,8 +50,8 @@ const CreateProjectRole = () => {
confetti: true, confetti: true,
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -95,9 +96,7 @@ const CreateProjectRole = () => {
validateNameUniqueness={validateNameUniqueness} validateNameUniqueness={validateNameUniqueness}
getRoleKey={getRoleKey} getRoleKey={getRoleKey}
> >
<PermissionButton permission={ADMIN} type="submit"> <CreateButton name="role" permission={ADMIN} />
Create role
</PermissionButton>
</ProjectRoleForm> </ProjectRoleForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -1,18 +1,16 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import FormTemplate from '../../../common/FormTemplate/FormTemplate'; import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useProjectRolesApi from '../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi'; import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
import { useHistory, useParams } from 'react-router-dom'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; import useToast from 'hooks/useToast';
import { IPermission } from 'interfaces/user';
import { useParams, useHistory } from 'react-router-dom';
import useProjectRoleForm from '../hooks/useProjectRoleForm'; import useProjectRoleForm from '../hooks/useProjectRoleForm';
import useProjectRole from '../../../../hooks/api/getters/useProjectRole/useProjectRole'; import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
import { IPermission } from '../../../../interfaces/project'; import { formatUnknownError } from 'utils/format-unknown-error';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
const EditProjectRole = () => { const EditProjectRole = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -88,8 +86,8 @@ const EditProjectRole = () => {
text: 'Your role changes will automatically be applied to the users with this role.', text: 'Your role changes will automatically be applied to the users with this role.',
confetti: true, confetti: true,
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -124,9 +122,7 @@ to resources within a project"
clearErrors={clearErrors} clearErrors={clearErrors}
getRoleKey={getRoleKey} getRoleKey={getRoleKey}
> >
<PermissionButton permission={ADMIN} type="submit"> <UpdateButton permission={ADMIN} />
Edit role
</PermissionButton>
</ProjectRoleForm> </ProjectRoleForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -1,16 +1,16 @@
import Input from '../../../common/Input/Input'; import Input from '../../../common/Input/Input';
import EnvironmentPermissionAccordion from './EnvironmentPermissionAccordion/EnvironmentPermissionAccordion'; import EnvironmentPermissionAccordion from './EnvironmentPermissionAccordion/EnvironmentPermissionAccordion';
import { import {
Button,
Checkbox, Checkbox,
FormControlLabel, FormControlLabel,
TextField, TextField,
Button,
} from '@material-ui/core'; } from '@material-ui/core';
import useProjectRolePermissions from '../../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from '../../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { useStyles } from './ProjectRoleForm.styles'; import { useStyles } from './ProjectRoleForm.styles';
import ConditionallyRender from '../../../common/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender';
import React from 'react'; import React, { ReactNode } from 'react';
import { IPermission } from '../../../../interfaces/project'; import { IPermission } from '../../../../interfaces/project';
import { import {
ICheckedPermission, ICheckedPermission,
@ -33,6 +33,7 @@ interface IProjectRoleForm {
clearErrors: () => void; clearErrors: () => void;
validateNameUniqueness?: () => void; validateNameUniqueness?: () => void;
getRoleKey: (permission: { id: number; environment?: string }) => string; getRoleKey: (permission: { id: number; environment?: string }) => string;
children: ReactNode;
} }
const ProjectRoleForm: React.FC<IProjectRoleForm> = ({ const ProjectRoleForm: React.FC<IProjectRoleForm> = ({

View File

@ -1,4 +1,4 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
deleteParagraph: { deleteParagraph: {

View File

@ -16,6 +16,7 @@ import IRole, { IProjectRole } from '../../../../../interfaces/role';
import useProjectRolesApi from '../../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi'; import useProjectRolesApi from '../../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import useToast from '../../../../../hooks/useToast'; import useToast from '../../../../../hooks/useToast';
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm'; import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
const ROOTROLE = 'root'; const ROOTROLE = 'root';
@ -44,8 +45,8 @@ const ProjectRoleList = () => {
title: 'Successfully deleted role', title: 'Successfully deleted role',
text: 'Your role is now deleted', text: 'Your role is now deleted',
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
setDelDialog(false); setDelDialog(false);
setConfirmName(''); setConfirmName('');

View File

@ -1,11 +1,12 @@
import { useStyles } from './ProjectRoleListItem.styles'; import { useStyles } from './ProjectRoleListItem.styles';
import { TableRow, TableCell, Typography } from '@material-ui/core'; import { TableCell, TableRow, Typography } from '@material-ui/core';
import { Edit, Delete } from '@material-ui/icons'; import { Delete, Edit } from '@material-ui/icons';
import { ADMIN } from '../../../../../providers/AccessProvider/permissions'; import { ADMIN } from '../../../../../providers/AccessProvider/permissions';
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'; import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
import PermissionIconButton from '../../../../../common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from '../../../../../common/PermissionIconButton/PermissionIconButton';
import { IProjectRole } from '../../../../../../interfaces/role'; import { IProjectRole } from '../../../../../../interfaces/role';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import React from 'react';
interface IRoleListItemProps { interface IRoleListItemProps {
id: number; id: number;
@ -50,7 +51,6 @@ const RoleListItem = ({
<PermissionIconButton <PermissionIconButton
data-loading data-loading
aria-label="Edit" aria-label="Edit"
tooltip="Edit"
disabled={type === BUILTIN_ROLE_TYPE} disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => { onClick={() => {
history.push(`/admin/roles/${id}/edit`); history.push(`/admin/roles/${id}/edit`);
@ -62,7 +62,6 @@ const RoleListItem = ({
<PermissionIconButton <PermissionIconButton
data-loading data-loading
aria-label="Remove role" aria-label="Remove role"
tooltip="Remove role"
disabled={type === BUILTIN_ROLE_TYPE} disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => { onClick={() => {
setCurrentRole({ id, name, description }); setCurrentRole({ id, name, description });

View File

@ -1,4 +1,4 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
rolesListBody: { rolesListBody: {

View File

@ -1,4 +1,4 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles({ export const useStyles = makeStyles({
iconContainer: { iconContainer: {

View File

@ -1,15 +1,16 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import UserForm from '../UserForm/UserForm'; import UserForm from '../UserForm/UserForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast'; import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import useToast from 'hooks/useToast';
import useAddUserForm from '../hooks/useAddUserForm'; import useAddUserForm from '../hooks/useAddUserForm';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded'; import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import { useState } from 'react'; import { useState } from 'react';
import { scrollToTop } from '../../../common/util'; import { scrollToTop } from '../../../common/util';
import PermissionButton from '../../../common/PermissionButton/PermissionButton'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
const CreateUser = () => { const CreateUser = () => {
const { setToastApiError } = useToast(); const { setToastApiError } = useToast();
@ -51,8 +52,8 @@ const CreateUser = () => {
setInviteLink(user.inviteLink); setInviteLink(user.inviteLink);
setShowConfirm(true); setShowConfirm(true);
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -97,9 +98,7 @@ const CreateUser = () => {
setRootRole={setRootRole} setRootRole={setRootRole}
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton permission={ADMIN} type="submit"> <CreateButton name="user" permission={ADMIN} />
Create user
</PermissionButton>
</UserForm> </UserForm>
<ConfirmUserAdded <ConfirmUserAdded
open={showConfirm} open={showConfirm}

View File

@ -1,16 +1,17 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import UserForm from '../UserForm/UserForm'; import UserForm from '../UserForm/UserForm';
import useAddUserForm from '../hooks/useAddUserForm'; import useAddUserForm from '../hooks/useAddUserForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import useUserInfo from '../../../../hooks/api/getters/useUserInfo/useUserInfo';
import { scrollToTop } from '../../../common/util'; import { scrollToTop } from '../../../common/util';
import { useEffect } from 'react'; import { useEffect } from 'react';
import PermissionButton from '../../../common/PermissionButton/PermissionButton'; import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { EDIT } from '../../../../constants/misc'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { EDIT } from 'constants/misc';
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/format-unknown-error';
const EditUser = () => { const EditUser = () => {
useEffect(() => { useEffect(() => {
@ -60,8 +61,8 @@ const EditUser = () => {
title: 'User information updated', title: 'User information updated',
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -94,9 +95,7 @@ const EditUser = () => {
clearErrors={clearErrors} clearErrors={clearErrors}
mode={EDIT} mode={EDIT}
> >
<PermissionButton permission={ADMIN} type="submit"> <UpdateButton permission={ADMIN} />
Edit user
</PermissionButton>
</UserForm> </UserForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -1,4 +1,4 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
userListBody: { userListBody: {

View File

@ -35,7 +35,7 @@ const UsersAdmin = () => {
history.push('/admin/create-user') history.push('/admin/create-user')
} }
> >
Add new user New user
</Button> </Button>
} }
elseShow={ elseShow={

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { TextField, Typography, Avatar } from '@material-ui/core'; import { Avatar, TextField, Typography } from '@material-ui/core';
import { trim } from '../../../../common/util'; import { trim } from '../../../../common/util';
import { modalStyles } from '../../util'; import { modalStyles } from '../../util';
import Dialogue from '../../../../common/Dialogue/Dialogue'; import Dialogue from '../../../../common/Dialogue/Dialogue';
@ -12,10 +12,10 @@ import { Alert } from '@material-ui/lab';
import { IUser } from '../../../../../interfaces/user'; import { IUser } from '../../../../../interfaces/user';
interface IChangePasswordProps { interface IChangePasswordProps {
showDialog: () => void; showDialog: boolean;
closeDialog: () => void; closeDialog: () => void;
changePassword: () => void; changePassword: (user: IUser, password: string) => Promise<Response>;
user: IUser; user: Partial<IUser>;
} }
const ChangePassword = ({ const ChangePassword = ({
@ -25,7 +25,7 @@ const ChangePassword = ({
user = {}, user = {},
}: IChangePasswordProps) => { }: IChangePasswordProps) => {
const [data, setData] = useState({}); const [data, setData] = useState({});
const [error, setError] = useState({}); const [error, setError] = useState<Record<string, string>>({});
const [validPassword, setValidPassword] = useState(false); const [validPassword, setValidPassword] = useState(false);
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
@ -88,7 +88,7 @@ const ChangePassword = ({
)} )}
> >
<ConditionallyRender <ConditionallyRender
condition={error.general} condition={Boolean(error.general)}
show={<Alert severity="error">{error.general}</Alert>} show={<Alert severity="error">{error.general}</Alert>}
/> />
<Typography variant="subtitle1"> <Typography variant="subtitle1">

View File

@ -9,12 +9,12 @@ import { useCommonStyles } from '../../../../../common.styles';
import { IUser } from '../../../../../interfaces/user'; import { IUser } from '../../../../../interfaces/user';
interface IDeleteUserProps { interface IDeleteUserProps {
showDialog: () => void; showDialog: boolean;
closeDialog: () => void; closeDialog: () => void;
user: IUser; user: IUser;
userLoading: boolean; userLoading: boolean;
removeUser: () => void; removeUser: () => void;
userApiErrors: Object; userApiErrors: Record<string, string>;
} }
const DeleteUser = ({ const DeleteUser = ({
@ -33,13 +33,13 @@ const DeleteUser = ({
open={showDialog} open={showDialog}
title="Really delete user?" title="Really delete user?"
onClose={closeDialog} onClose={closeDialog}
onClick={() => removeUser(user)} onClick={removeUser}
primaryButtonText="Delete user" primaryButtonText="Delete user"
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
> >
<div ref={ref}> <div ref={ref}>
<ConditionallyRender <ConditionallyRender
condition={userApiErrors[REMOVE_USER_ERROR]} condition={Boolean(userApiErrors[REMOVE_USER_ERROR])}
show={ show={
<Alert <Alert
data-loading data-loading

View File

@ -1,25 +1,24 @@
import { import {
TableRow,
TableCell,
Avatar, Avatar,
IconButton, IconButton,
TableCell,
TableRow,
Typography, Typography,
} from '@material-ui/core'; } from '@material-ui/core';
import { Edit, Lock, Delete } from '@material-ui/icons'; import { Delete, Edit, Lock } from '@material-ui/icons';
import { SyntheticEvent, useContext } from 'react'; import { SyntheticEvent, useContext } from 'react';
import { ADMIN } from '../../../../providers/AccessProvider/permissions'; import { ADMIN } from '../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import { formatDateWithLocale } from '../../../../common/util';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import { IUser } from '../../../../../interfaces/user'; import { IUser } from '../../../../../interfaces/user';
import { useStyles } from './UserListItem.styles'; import { useStyles } from './UserListItem.styles';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { ILocationSettings } from '../../../../../hooks/useLocationSettings'; import { ILocationSettings } from '../../../../../hooks/useLocationSettings';
import { formatDateYMD } from '../../../../../utils/format-date';
interface IUserListItemProps { interface IUserListItemProps {
user: IUser; user: IUser;
renderRole: (roleId: number) => string; renderRole: (roleId: number) => string;
openUpdateDialog: (user: IUser) => (e: SyntheticEvent) => void;
openPwDialog: (user: IUser) => (e: SyntheticEvent) => void; openPwDialog: (user: IUser) => (e: SyntheticEvent) => void;
openDelDialog: (user: IUser) => (e: SyntheticEvent) => void; openDelDialog: (user: IUser) => (e: SyntheticEvent) => void;
locationSettings: ILocationSettings; locationSettings: ILocationSettings;
@ -30,7 +29,6 @@ const UserListItem = ({
renderRole, renderRole,
openDelDialog, openDelDialog,
openPwDialog, openPwDialog,
openUpdateDialog,
locationSettings, locationSettings,
}: IUserListItemProps) => { }: IUserListItemProps) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -51,10 +49,7 @@ const UserListItem = ({
</TableCell> </TableCell>
<TableCell> <TableCell>
<span data-loading> <span data-loading>
{formatDateWithLocale( {formatDateYMD(user.createdAt, locationSettings.locale)}
user.createdAt,
locationSettings.locale
)}
</span> </span>
</TableCell> </TableCell>
<TableCell className={styles.leftTableCell}> <TableCell className={styles.leftTableCell}>

View File

@ -24,6 +24,7 @@ import { IUser } from '../../../../interfaces/user';
import IRole from '../../../../interfaces/role'; import IRole from '../../../../interfaces/role';
import useToast from '../../../../hooks/useToast'; import useToast from '../../../../hooks/useToast';
import { useLocationSettings } from '../../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../../hooks/useLocationSettings';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
const UsersList = () => { const UsersList = () => {
const { users, roles, refetch, loading } = useUsers(); const { users, roles, refetch, loading } = useUsers();
@ -79,8 +80,8 @@ const UsersList = () => {
}); });
refetch(); refetch();
closeDelDialog(); closeDelDialog();
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -172,7 +173,7 @@ const UsersList = () => {
<DeleteUser <DeleteUser
showDialog={delDialog} showDialog={delDialog}
closeDialog={closeDelDialog} closeDialog={closeDelDialog}
user={delUser} user={delUser!}
removeUser={onDeleteUser} removeUser={onDeleteUser}
userLoading={userLoading} userLoading={userLoading}
userApiErrors={userApiErrors} userApiErrors={userApiErrors}

View File

@ -1,16 +1,15 @@
/* eslint react/no-multi-comp:off */ /* eslint react/no-multi-comp:off */
import { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { import {
Avatar, Avatar,
Link,
Icon, Icon,
IconButton, IconButton,
LinearProgress, LinearProgress,
Link,
Typography, Typography,
} from '@material-ui/core'; } from '@material-ui/core';
import { Link as LinkIcon } from '@material-ui/icons'; import { Link as LinkIcon } from '@material-ui/icons';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { formatDateWithLocale } from '../../common/util';
import { UPDATE_APPLICATION } from '../../providers/AccessProvider/permissions'; import { UPDATE_APPLICATION } from '../../providers/AccessProvider/permissions';
import { ApplicationView } from '../ApplicationView/ApplicationView'; import { ApplicationView } from '../ApplicationView/ApplicationView';
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate'; import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
@ -25,6 +24,8 @@ import { useHistory, useParams } from 'react-router-dom';
import { useLocationSettings } from '../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../hooks/useLocationSettings';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import PermissionButton from '../../common/PermissionButton/PermissionButton'; import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { formatDateYMD } from '../../../utils/format-date';
import { formatUnknownError } from '../../../utils/format-unknown-error';
export const ApplicationEdit = () => { export const ApplicationEdit = () => {
const history = useHistory(); const history = useHistory();
@ -42,10 +43,9 @@ export const ApplicationEdit = () => {
setShowDialog(!showDialog); setShowDialog(!showDialog);
}; };
const formatDate = (v: string) => const formatDate = (v: string) => formatDateYMD(v, locationSettings.locale);
formatDateWithLocale(v, locationSettings.locale);
const onDeleteApplication = async (evt: Event) => { const onDeleteApplication = async (evt: React.SyntheticEvent) => {
evt.preventDefault(); evt.preventDefault();
try { try {
await deleteApplication(appName); await deleteApplication(appName);
@ -55,8 +55,8 @@ export const ApplicationEdit = () => {
type: 'success', type: 'success',
}); });
history.push('/applications'); history.push('/applications');
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -1,5 +1,5 @@
import { ChangeEvent, useState } from 'react'; import { ChangeEvent, useState } from 'react';
import { TextField, Grid } from '@material-ui/core'; import { Grid, TextField } from '@material-ui/core';
import { useCommonStyles } from '../../../common.styles'; import { useCommonStyles } from '../../../common.styles';
import icons from '../icon-names'; import icons from '../icon-names';
import GeneralSelect from '../../common/GeneralSelect/GeneralSelect'; import GeneralSelect from '../../common/GeneralSelect/GeneralSelect';
@ -7,6 +7,7 @@ import useApplicationsApi from '../../../hooks/api/actions/useApplicationsApi/us
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import { IApplication } from '../../../interfaces/application'; import { IApplication } from '../../../interfaces/application';
import useApplication from '../../../hooks/api/getters/useApplication/useApplication'; import useApplication from '../../../hooks/api/getters/useApplication/useApplication';
import { formatUnknownError } from '../../../utils/format-unknown-error';
interface IApplicationUpdateProps { interface IApplicationUpdateProps {
application: IApplication; application: IApplication;
@ -35,8 +36,8 @@ export const ApplicationUpdate = ({ application }: IApplicationUpdateProps) => {
title: 'Updated Successfully', title: 'Updated Successfully',
text: `${field} successfully updated`, text: `${field} successfully updated`,
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -4,16 +4,16 @@ import {
Grid, Grid,
List, List,
ListItem, ListItem,
ListItemText,
ListItemAvatar, ListItemAvatar,
ListItemText,
Typography, Typography,
} from '@material-ui/core'; } from '@material-ui/core';
import { import {
Report,
Extension, Extension,
Timeline,
FlagRounded, FlagRounded,
Report,
SvgIconComponent, SvgIconComponent,
Timeline,
} from '@material-ui/icons'; } from '@material-ui/icons';
import { import {
CREATE_FEATURE, CREATE_FEATURE,
@ -23,13 +23,16 @@ import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyR
import { getTogglePath } from '../../../utils/route-path-helpers'; import { getTogglePath } from '../../../utils/route-path-helpers';
import useApplication from '../../../hooks/api/getters/useApplication/useApplication'; import useApplication from '../../../hooks/api/getters/useApplication/useApplication';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { formatFullDateTimeWithLocale } from '../../common/util'; import { formatDateYMDHMS } from '../../../utils/format-date';
import { useLocationSettings } from '../../../hooks/useLocationSettings';
export const ApplicationView = () => { export const ApplicationView = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { name } = useParams<{ name: string }>(); const { name } = useParams<{ name: string }>();
const { application } = useApplication(name); const { application } = useApplication(name);
const { locationSettings } = useLocationSettings();
const { instances, strategies, seenToggles } = application; const { instances, strategies, seenToggles } = application;
const notFoundListItem = ({ const notFoundListItem = ({
createUrl, createUrl,
name, name,
@ -114,10 +117,9 @@ export const ApplicationView = () => {
createUrl: `/projects/default/create-toggle?name=${name}`, createUrl: `/projects/default/create-toggle?name=${name}`,
name, name,
permission: CREATE_FEATURE, permission: CREATE_FEATURE,
i,
})} })}
elseShow={foundListItem({ elseShow={foundListItem({
viewUrl: getTogglePath(project, name, true), viewUrl: getTogglePath(project, name),
name, name,
description, description,
Icon: FlagRounded, Icon: FlagRounded,
@ -195,8 +197,9 @@ export const ApplicationView = () => {
<span> <span>
{clientIp} last seen at{' '} {clientIp} last seen at{' '}
<small> <small>
{formatFullDateTimeWithLocale( {formatDateYMDHMS(
lastSeen lastSeen,
locationSettings.locale
)} )}
</small> </small>
</span> </span>

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useRef, FC } from 'react'; import React, { useEffect, useState, useRef, FC } from 'react';
import ConditionallyRender from '../ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender';
interface IAnimateOnMountProps { interface IAnimateOnMountProps {
@ -7,7 +7,7 @@ interface IAnimateOnMountProps {
start: string; start: string;
leave: string; leave: string;
container?: string; container?: string;
style?: Object; style?: React.CSSProperties;
} }
const AnimateOnMount: FC<IAnimateOnMountProps> = ({ const AnimateOnMount: FC<IAnimateOnMountProps> = ({

View File

@ -1,5 +1,6 @@
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import React from 'react';
interface IApiErrorProps { interface IApiErrorProps {
className?: string; className?: string;

View File

@ -1,9 +1,10 @@
interface IConditionallyRenderProps { interface IConditionallyRenderProps {
condition: boolean; condition: boolean;
show: JSX.Element | RenderFunc; show: TargetElement;
elseShow?: JSX.Element | RenderFunc; elseShow?: TargetElement;
} }
type TargetElement = JSX.Element | JSX.Element[] | RenderFunc | null;
type RenderFunc = () => JSX.Element; type RenderFunc = () => JSX.Element;
const ConditionallyRender = ({ const ConditionallyRender = ({
@ -23,8 +24,9 @@ const ConditionallyRender = ({
return result; return result;
}; };
const isFunc = (param: JSX.Element | RenderFunc) => const isFunc = (param: TargetElement): boolean => {
typeof param === 'function'; return typeof param === 'function';
};
if (condition) { if (condition) {
if (isFunc(show)) { if (isFunc(show)) {

View File

@ -52,7 +52,6 @@ const Constraint = ({
<div className={styles.btnContainer}> <div className={styles.btnContainer}>
<PermissionIconButton <PermissionIconButton
onClick={editCallback} onClick={editCallback}
tooltip="Edit strategy"
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId} projectId={projectId}
> >
@ -61,7 +60,6 @@ const Constraint = ({
<PermissionIconButton <PermissionIconButton
onClick={deleteCallback} onClick={deleteCallback}
tooltip="Delete strategy"
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId} projectId={projectId}
> >

View File

@ -0,0 +1,15 @@
import PermissionButton, {
IPermissionButtonProps,
} from '../PermissionButton/PermissionButton';
interface ICreateButtonProps extends IPermissionButtonProps {
name: string;
}
export const CreateButton = ({ name, ...rest }: ICreateButtonProps) => {
return (
<PermissionButton type="submit" {...rest}>
Create {name}
</PermissionButton>
);
};

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { import {
Button,
Dialog, Dialog,
DialogTitle,
DialogActions, DialogActions,
DialogContent, DialogContent,
Button, DialogTitle,
} from '@material-ui/core'; } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
@ -15,15 +15,15 @@ interface IDialogue {
primaryButtonText?: string; primaryButtonText?: string;
secondaryButtonText?: string; secondaryButtonText?: string;
open: boolean; open: boolean;
onClick: (e: any) => void; onClick: (e: React.SyntheticEvent) => void;
onClose: () => void; onClose?: (e: React.SyntheticEvent) => void;
style?: object; style?: object;
title: string; title: string;
fullWidth?: boolean; fullWidth?: boolean;
maxWidth?: 'lg' | 'sm' | 'xs' | 'md' | 'xl'; maxWidth?: 'lg' | 'sm' | 'xs' | 'md' | 'xl';
disabledPrimaryButton?: boolean; disabledPrimaryButton?: boolean;
formId?: string; formId?: string;
permissionButton?: React.ReactNode; permissionButton?: JSX.Element;
} }
const Dialogue: React.FC<IDialogue> = ({ const Dialogue: React.FC<IDialogue> = ({
@ -69,7 +69,7 @@ const Dialogue: React.FC<IDialogue> = ({
<DialogActions> <DialogActions>
<ConditionallyRender <ConditionallyRender
condition={Boolean(permissionButton)} condition={Boolean(permissionButton)}
show={permissionButton} show={permissionButton!}
elseShow={ elseShow={
<ConditionallyRender <ConditionallyRender
condition={Boolean(onClick)} condition={Boolean(onClick)}

View File

@ -1,3 +1,5 @@
import React from 'react';
interface EnvironmentSplashPageProps { interface EnvironmentSplashPageProps {
title: React.ReactNode; title: React.ReactNode;
topDescription: React.ReactNode; topDescription: React.ReactNode;

View File

@ -7,6 +7,7 @@ import ConditionallyRender from '../ConditionallyRender';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import React from 'react';
interface ICreateProps { interface ICreateProps {
title: string; title: string;

View File

@ -1,3 +1,5 @@
import React from 'react';
interface IGradientProps { interface IGradientProps {
from: string; from: string;
to: string; to: string;

View File

@ -1,6 +1,7 @@
import { TextField } from '@material-ui/core'; import { TextField } from '@material-ui/core';
import { INPUT_ERROR_TEXT } from '../../../testIds'; import { INPUT_ERROR_TEXT } from '../../../testIds';
import { useStyles } from './Input.styles.ts'; import { useStyles } from './Input.styles';
import React from 'react';
interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string; label: string;

View File

@ -1,5 +1,6 @@
import { ReactComponent as NoItemsIcon } from '../../../assets/icons/addfiles.svg'; import { ReactComponent as NoItemsIcon } from '../../../assets/icons/addfiles.svg';
import { useStyles } from './NoItems.styles'; import { useStyles } from './NoItems.styles';
import React from 'react';
const NoItems: React.FC = ({ children }) => { const NoItems: React.FC = ({ children }) => {
const styles = useStyles(); const styles = useStyles();

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import ConditionallyRender from '../ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender';
import classnames from 'classnames'; import classnames from 'classnames';
import { useStyles } from './PaginationUI.styles'; import { useStyles } from './PaginationUI.styles';

View File

@ -1,6 +1,6 @@
import { IconButton, InputAdornment, TextField } from '@material-ui/core'; import { IconButton, InputAdornment, TextField } from '@material-ui/core';
import { Visibility, VisibilityOff } from '@material-ui/icons'; import { Visibility, VisibilityOff } from '@material-ui/icons';
import { useState } from 'react'; import React, { useState } from 'react';
const PasswordField = ({ ...rest }) => { const PasswordField = ({ ...rest }) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);

View File

@ -1,10 +1,10 @@
import { Button, Tooltip } from '@material-ui/core'; import { Button, Tooltip } from '@material-ui/core';
import { Lock } from '@material-ui/icons'; import { Lock } from '@material-ui/icons';
import { useContext } from 'react'; import AccessContext from 'contexts/AccessContext';
import AccessContext from '../../../contexts/AccessContext'; import React, { useContext } from 'react';
import ConditionallyRender from '../ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender';
export interface IPermissionIconButtonProps export interface IPermissionButtonProps
extends React.HTMLProps<HTMLButtonElement> { extends React.HTMLProps<HTMLButtonElement> {
permission: string | string[]; permission: string | string[];
tooltip?: string; tooltip?: string;
@ -14,9 +14,9 @@ export interface IPermissionIconButtonProps
environmentId?: string; environmentId?: string;
} }
const PermissionButton: React.FC<IPermissionIconButtonProps> = ({ const PermissionButton: React.FC<IPermissionButtonProps> = ({
permission, permission,
tooltip = 'Click to perform action', tooltip,
onClick, onClick,
children, children,
disabled, disabled,
@ -54,9 +54,9 @@ const PermissionButton: React.FC<IPermissionIconButtonProps> = ({
access = handleAccess(); access = handleAccess();
const tooltipText = access const tooltipText = !access
? tooltip ? "You don't have access to perform this operation"
: "You don't have access to perform this operation"; : '';
return ( return (
<Tooltip title={tooltipText} arrow> <Tooltip title={tooltipText} arrow>

View File

@ -1,5 +1,5 @@
import { IconButton, Tooltip } from '@material-ui/core'; import { IconButton, Tooltip } from '@material-ui/core';
import { useContext } from 'react'; import React, { useContext } from 'react';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
interface IPermissionIconButtonProps interface IPermissionIconButtonProps
@ -39,9 +39,9 @@ const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
access = hasAccess(permission); access = hasAccess(permission);
} }
const tooltipText = access const tooltipText = !access
? tooltip || '' ? "You don't have access to perform this operation"
: "You don't have access to perform this operation"; : '';
return ( return (
<Tooltip title={tooltipText} arrow> <Tooltip title={tooltipText} arrow>

View File

@ -19,7 +19,7 @@ const PermissionSwitch = React.forwardRef<
>((props, ref) => { >((props, ref) => {
const { const {
permission, permission,
tooltip = '', tooltip,
disabled, disabled,
projectId, projectId,
environmentId, environmentId,
@ -39,9 +39,9 @@ const PermissionSwitch = React.forwardRef<
access = hasAccess(permission); access = hasAccess(permission);
} }
const tooltipText = access const tooltipText = !access
? tooltip ? "You don't have access to perform this operation"
: "You don't have access to perform this operation"; : '';
return ( return (
<Tooltip title={tooltipText} arrow> <Tooltip title={tooltipText} arrow>

View File

@ -2,6 +2,7 @@ import { useMediaQuery } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender';
import PermissionButton from '../PermissionButton/PermissionButton'; import PermissionButton from '../PermissionButton/PermissionButton';
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton'; import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
import React from 'react';
interface IResponsiveButtonProps { interface IResponsiveButtonProps {
Icon: React.ElementType; Icon: React.ElementType;

View File

@ -1,12 +1,11 @@
import { Fragment } from 'react'; import React, { Fragment, useState } from 'react';
import { Button, IconButton } from '@material-ui/core'; import { Button, IconButton } from '@material-ui/core';
import { useStyles } from './Splash.styles'; import { useStyles } from './Splash.styles';
import { import {
CloseOutlined,
FiberManualRecord, FiberManualRecord,
FiberManualRecordOutlined, FiberManualRecordOutlined,
CloseOutlined,
} from '@material-ui/icons'; } from '@material-ui/icons';
import { useState } from 'react';
import ConditionallyRender from '../ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender';
import { CLOSE_SPLASH } from '../../../testIds'; import { CLOSE_SPLASH } from '../../../testIds';

View File

@ -7,7 +7,7 @@ interface ITagSelect extends React.SelectHTMLAttributes<HTMLSelectElement> {
onChange: (val: any) => void; onChange: (val: any) => void;
} }
const TagSelect = ({ value, types, onChange, ...rest }: ITagSelect) => { const TagSelect = ({ value, onChange, ...rest }: ITagSelect) => {
const { tagTypes } = useTagTypes(); const { tagTypes } = useTagTypes();
const options = tagTypes.map(tagType => ({ const options = tagTypes.map(tagType => ({

View File

@ -3,12 +3,12 @@ import classnames from 'classnames';
import { useContext } from 'react'; import { useContext } from 'react';
import { IconButton } from '@material-ui/core'; import { IconButton } from '@material-ui/core';
import CheckMarkBadge from '../../CheckmarkBadge/CheckMarkBadge'; import CheckMarkBadge from '../../CheckmarkBadge/CheckMarkBadge';
import UIContext, { IToastData } from '../../../../contexts/UIContext'; import UIContext from '../../../../contexts/UIContext';
import ConditionallyRender from '../../ConditionallyRender'; import ConditionallyRender from '../../ConditionallyRender';
import Close from '@material-ui/icons/Close'; import Close from '@material-ui/icons/Close';
import { IToast } from '../../../../interfaces/toast';
const Toast = ({ title, text, type, confetti }: IToastData) => { const Toast = ({ title, text, type, confetti }: IToast) => {
// @ts-expect-error
const { setToast } = useContext(UIContext); const { setToast } = useContext(UIContext);
const styles = useStyles(); const styles = useStyles();
@ -51,7 +51,7 @@ const Toast = ({ title, text, type, confetti }: IToastData) => {
}; };
const hide = () => { const hide = () => {
setToast((prev: IToastData) => ({ ...prev, show: false })); setToast((prev: IToast) => ({ ...prev, show: false }));
}; };
return ( return (

View File

@ -1,19 +1,19 @@
import { Portal } from '@material-ui/core'; import { Portal } from '@material-ui/core';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { useCommonStyles } from '../../../common.styles'; import { useCommonStyles } from '../../../common.styles';
import UIContext, { IToastData } from '../../../contexts/UIContext'; import UIContext from '../../../contexts/UIContext';
import { useStyles } from './ToastRenderer.styles'; import { useStyles } from './ToastRenderer.styles';
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount'; import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
import Toast from './Toast/Toast'; import Toast from './Toast/Toast';
import { IToast } from '../../../interfaces/toast';
const ToastRenderer = () => { const ToastRenderer = () => {
// @ts-expect-error
const { toastData, setToast } = useContext(UIContext); const { toastData, setToast } = useContext(UIContext);
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
const styles = useStyles(); const styles = useStyles();
const hide = () => { const hide = () => {
setToast((prev: IToastData) => ({ ...prev, show: false })); setToast((prev: IToast) => ({ ...prev, show: false }));
}; };
useEffect(() => { useEffect(() => {
@ -31,7 +31,7 @@ const ToastRenderer = () => {
return ( return (
<Portal> <Portal>
<AnimateOnMount <AnimateOnMount
mounted={toastData?.show} mounted={Boolean(toastData?.show)}
start={commonStyles.fadeInBottomStartWithoutFixed} start={commonStyles.fadeInBottomStartWithoutFixed}
enter={commonStyles.fadeInBottomEnter} enter={commonStyles.fadeInBottomEnter}
leave={commonStyles.fadeInBottomLeave} leave={commonStyles.fadeInBottomLeave}

View File

@ -0,0 +1,11 @@
import PermissionButton, {
IPermissionButtonProps,
} from '../PermissionButton/PermissionButton';
export const UpdateButton = ({ ...rest }: IPermissionButtonProps) => {
return (
<PermissionButton type="submit" {...rest}>
Save
</PermissionButton>
);
};

View File

@ -1,35 +0,0 @@
import { formatFullDateTimeWithLocale } from '../util';
test.skip('formats dates correctly', () => {
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'UTC')).toEqual(
'2017-02-23 14:56:49'
);
expect(
formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Paris')
).toEqual('2017-02-23 15:56:49');
expect(
formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Oslo')
).toEqual('2017-02-23 15:56:49');
expect(
formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/London')
).toEqual('2017-02-23 14:56:49');
expect(
formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Paris')
).toEqual('02/23/2017, 3:56:49 PM');
expect(
formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Oslo')
).toEqual('02/23/2017, 3:56:49 PM');
expect(
formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/London')
).toEqual('02/23/2017, 2:56:49 PM');
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO')).toEqual(
expect.stringMatching(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/)
);
expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB')).toEqual(
expect.stringContaining('02/23/2017')
);
expect(formatFullDateTimeWithLocale(1487861809466, 'en-US')).toEqual(
expect.stringContaining('02/23/2017')
);
});

View File

@ -1,26 +1,6 @@
import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums'; import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums';
import differenceInDays from 'date-fns/differenceInDays'; import differenceInDays from 'date-fns/differenceInDays';
const dateTimeOptions = {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
};
const dateOptions = {
day: '2-digit',
month: '2-digit',
year: 'numeric',
};
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
};
export const filterByFlags = flags => r => { export const filterByFlags = flags => r => {
if (r.flag && !flags[r.flag]) { if (r.flag && !flags[r.flag]) {
return false; return false;
@ -32,27 +12,6 @@ export const scrollToTop = () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}; };
export const formatFullDateTimeWithLocale = (v, locale, tz) => {
if (tz) {
dateTimeOptions.timeZone = tz;
}
return new Date(v).toLocaleString(locale, dateTimeOptions);
};
export const formatDateWithLocale = (v, locale, tz) => {
if (tz) {
dateTimeOptions.timeZone = tz;
}
return new Date(v).toLocaleString(locale, dateOptions);
};
export const formatTimeWithLocale = (v, locale, tz) => {
if (tz) {
dateTimeOptions.timeZone = tz;
}
return new Date(v).toLocaleString(locale, timeOptions);
};
export const trim = value => { export const trim = value => {
if (value && value.trim) { if (value && value.trim) {
return value.trim(); return value.trim();

View File

@ -1,9 +1,9 @@
import Input from '../../common/Input/Input'; import Input from 'component/common/Input/Input';
import { TextField, Button, Switch, Chip, Typography } from '@material-ui/core'; import { TextField, Button, Switch, Chip, Typography } from '@material-ui/core';
import { useStyles } from './ContextForm.styles'; import { useStyles } from './ContextForm.styles';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Add } from '@material-ui/icons'; import { Add } from '@material-ui/icons';
import { trim } from '../../common/util'; import { trim } from 'component/common/util';
interface IContextForm { interface IContextForm {
contextName: string; contextName: string;
@ -15,20 +15,20 @@ interface IContextForm {
setStickiness: React.Dispatch<React.SetStateAction<boolean>>; setStickiness: React.Dispatch<React.SetStateAction<boolean>>;
setLegalValues: React.Dispatch<React.SetStateAction<string[]>>; setLegalValues: React.Dispatch<React.SetStateAction<string[]>>;
handleSubmit: (e: any) => void; handleSubmit: (e: any) => void;
handleCancel: () => void; onCancel: () => void;
errors: { [key: string]: string }; errors: { [key: string]: string };
mode: string; mode: string;
clearErrors: () => void; clearErrors: () => void;
validateNameUniqueness: () => void; validateContext?: () => void;
setErrors: React.Dispatch<React.SetStateAction<Object>>; setErrors: React.Dispatch<React.SetStateAction<Object>>;
} }
const ENTER = 'Enter'; const ENTER = 'Enter';
const ContextForm: React.FC<IContextForm> = ({ export const ContextForm: React.FC<IContextForm> = ({
children, children,
handleSubmit, handleSubmit,
handleCancel, onCancel,
contextName, contextName,
contextDesc, contextDesc,
legalValues, legalValues,
@ -39,7 +39,7 @@ const ContextForm: React.FC<IContextForm> = ({
setStickiness, setStickiness,
errors, errors,
mode, mode,
validateNameUniqueness, validateContext,
setErrors, setErrors,
clearErrors, clearErrors,
}) => { }) => {
@ -108,7 +108,7 @@ const ContextForm: React.FC<IContextForm> = ({
error={Boolean(errors.name)} error={Boolean(errors.name)}
errorText={errors.name} errorText={errors.name}
onFocus={() => clearErrors()} onFocus={() => clearErrors()}
onBlur={validateNameUniqueness} onBlur={validateContext}
autoFocus autoFocus
/> />
<p className={styles.inputDescription}> <p className={styles.inputDescription}>
@ -187,12 +187,10 @@ const ContextForm: React.FC<IContextForm> = ({
</div> </div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
{children} {children}
<Button onClick={handleCancel} className={styles.cancelButton}> <Button onClick={onCancel} className={styles.cancelButton}>
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>
); );
}; };
export default ContextForm;

View File

@ -7,6 +7,7 @@ import {
UPDATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD,
} from '../../providers/AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
import { import {
Button,
IconButton, IconButton,
List, List,
ListItem, ListItem,
@ -14,7 +15,6 @@ import {
ListItemText, ListItemText,
Tooltip, Tooltip,
useMediaQuery, useMediaQuery,
Button,
} from '@material-ui/core'; } from '@material-ui/core';
import { Add, Album, Delete, Edit } from '@material-ui/icons'; import { Add, Album, Delete, Edit } from '@material-ui/icons';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
@ -25,6 +25,7 @@ import AccessContext from '../../../contexts/AccessContext';
import useUnleashContext from '../../../hooks/api/getters/useUnleashContext/useUnleashContext'; import useUnleashContext from '../../../hooks/api/getters/useUnleashContext/useUnleashContext';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi'; import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const ContextList = () => { const ContextList = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -46,8 +47,8 @@ const ContextList = () => {
title: 'Successfully deleted context', title: 'Successfully deleted context',
text: 'Your context is now deleted', text: 'Your context is now deleted',
}); });
} catch (e) { } catch (error) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
setName(undefined); setName(undefined);
setShowDelDialogue(false); setShowDelDialogue(false);
@ -127,7 +128,7 @@ const ContextList = () => {
color="primary" color="primary"
variant="contained" variant="contained"
> >
Add new context field New context field
</Button> </Button>
} }
/> />

View File

@ -1,15 +1,16 @@
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useToast from '../../../hooks/useToast'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import FormTemplate from '../../common/FormTemplate/FormTemplate'; import { useContextForm } from '../hooks/useContextForm';
import useContextForm from '../hooks/useContextForm'; import { ContextForm } from '../ContextForm/ContextForm';
import ContextForm from '../ContextForm/ContextForm'; import { CREATE_CONTEXT_FIELD } from 'component/providers/AccessProvider/permissions';
import PermissionButton from '../../common/PermissionButton/PermissionButton'; import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import { CREATE_CONTEXT_FIELD } from '../../providers/AccessProvider/permissions'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import useUnleashContext from '../../../hooks/api/getters/useUnleashContext/useUnleashContext'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/format-unknown-error';
const CreateContext = () => { export const CreateContext = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const history = useHistory(); const history = useHistory();
@ -23,8 +24,7 @@ const CreateContext = () => {
setLegalValues, setLegalValues,
setStickiness, setStickiness,
getContextPayload, getContextPayload,
validateNameUniqueness, validateContext,
validateName,
clearErrors, clearErrors,
setErrors, setErrors,
errors, errors,
@ -34,7 +34,8 @@ const CreateContext = () => {
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
const validName = validateName(); const validName = await validateContext();
if (validName) { if (validName) {
const payload = getContextPayload(); const payload = getContextPayload();
try { try {
@ -46,8 +47,8 @@ const CreateContext = () => {
confetti: true, confetti: true,
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -61,7 +62,7 @@ const CreateContext = () => {
--data-raw '${JSON.stringify(getContextPayload(), undefined, 2)}'`; --data-raw '${JSON.stringify(getContextPayload(), undefined, 2)}'`;
}; };
const handleCancel = () => { const onCancel = () => {
history.goBack(); history.goBack();
}; };
@ -77,7 +78,7 @@ const CreateContext = () => {
<ContextForm <ContextForm
errors={errors} errors={errors}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={handleCancel} onCancel={onCancel}
contextName={contextName} contextName={contextName}
setContextName={setContextName} setContextName={setContextName}
contextDesc={contextDesc} contextDesc={contextDesc}
@ -87,19 +88,15 @@ const CreateContext = () => {
stickiness={stickiness} stickiness={stickiness}
setStickiness={setStickiness} setStickiness={setStickiness}
mode="Create" mode="Create"
validateNameUniqueness={validateNameUniqueness} validateContext={validateContext}
setErrors={setErrors} setErrors={setErrors}
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton <CreateButton
name="context"
permission={CREATE_CONTEXT_FIELD} permission={CREATE_CONTEXT_FIELD}
type="submit" />
>
Create context
</PermissionButton>
</ContextForm> </ContextForm>
</FormTemplate> </FormTemplate>
); );
}; };
export default CreateContext;

View File

@ -1,17 +1,18 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { UPDATE_CONTEXT_FIELD } from 'component/providers/AccessProvider/permissions';
import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import useContext from 'hooks/api/getters/useContext/useContext';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
import useContext from '../../../hooks/api/getters/useContext/useContext';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { scrollToTop } from '../../common/util'; import { scrollToTop } from '../../common/util';
import { UPDATE_CONTEXT_FIELD } from '../../providers/AccessProvider/permissions'; import { formatUnknownError } from 'utils/format-unknown-error';
import ContextForm from '../ContextForm/ContextForm'; import { ContextForm } from '../ContextForm/ContextForm';
import useContextForm from '../hooks/useContextForm'; import { useContextForm } from '../hooks/useContextForm';
const EditContext = () => { export const EditContext = () => {
useEffect(() => { useEffect(() => {
scrollToTop(); scrollToTop();
}, []); }, []);
@ -32,8 +33,6 @@ const EditContext = () => {
setLegalValues, setLegalValues,
setStickiness, setStickiness,
getContextPayload, getContextPayload,
validateNameUniqueness,
validateName,
clearErrors, clearErrors,
setErrors, setErrors,
errors, errors,
@ -56,9 +55,7 @@ const EditContext = () => {
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
const payload = getContextPayload(); const payload = getContextPayload();
const validName = validateName();
if (validName) {
try { try {
await updateContext(payload); await updateContext(payload);
refetch(); refetch();
@ -67,13 +64,12 @@ const EditContext = () => {
title: 'Context information updated', title: 'Context information updated',
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (e: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(e));
}
} }
}; };
const handleCancel = () => { const onCancel = () => {
history.goBack(); history.goBack();
}; };
@ -89,7 +85,7 @@ const EditContext = () => {
<ContextForm <ContextForm
errors={errors} errors={errors}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={handleCancel} onCancel={onCancel}
contextName={contextName} contextName={contextName}
setContextName={setContextName} setContextName={setContextName}
contextDesc={contextDesc} contextDesc={contextDesc}
@ -99,19 +95,11 @@ const EditContext = () => {
stickiness={stickiness} stickiness={stickiness}
setStickiness={setStickiness} setStickiness={setStickiness}
mode="Edit" mode="Edit"
validateNameUniqueness={validateNameUniqueness}
setErrors={setErrors} setErrors={setErrors}
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton <UpdateButton permission={UPDATE_CONTEXT_FIELD} />
permission={UPDATE_CONTEXT_FIELD}
type="submit"
>
Edit context
</PermissionButton>
</ContextForm> </ContextForm>
</FormTemplate> </FormTemplate>
); );
}; };
export default EditContext;

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi'; import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
const useContextForm = ( export const useContextForm = (
initialcontextName = '', initialcontextName = '',
initialcontextDesc = '', initialcontextDesc = '',
initialLegalValues = [] as string[], initialLegalValues = [] as string[],
@ -42,25 +42,28 @@ const useContextForm = (
const NAME_EXISTS_ERROR = 'A context field with that name already exist'; const NAME_EXISTS_ERROR = 'A context field with that name already exist';
const validateNameUniqueness = async () => { const validateContext = async () => {
if (contextName.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
return false;
}
try { try {
await validateContextName(contextName); await validateContextName(contextName);
return true;
} catch (e: any) { } catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) { if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({ setErrors(prev => ({
...prev, ...prev,
name: 'A context field with that name already exist', name: 'A context field with that name already exist',
})); }));
} else {
setErrors(prev => ({
...prev,
name: e.toString(),
}));
} }
}
};
const validateName = () => {
if (contextName.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
return false; return false;
} }
return true;
}; };
const clearErrors = () => { const clearErrors = () => {
@ -77,12 +80,9 @@ const useContextForm = (
setLegalValues, setLegalValues,
setStickiness, setStickiness,
getContextPayload, getContextPayload,
validateNameUniqueness, validateContext,
validateName,
setErrors, setErrors,
clearErrors, clearErrors,
errors, errors,
}; };
}; };
export default useContextForm;

View File

@ -1,19 +1,20 @@
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useEnvironmentForm from '../hooks/useEnvironmentForm'; import useEnvironmentForm from '../hooks/useEnvironmentForm';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm'; import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import FormTemplate from '../../common/FormTemplate/FormTemplate'; import FormTemplate from '../../common/FormTemplate/FormTemplate';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';
import ConditionallyRender from '../../common/ConditionallyRender'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import PageContent from '../../common/PageContent'; import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import HeaderTitle from '../../common/HeaderTitle'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import PermissionButton from '../../common/PermissionButton/PermissionButton'; import useToast from 'hooks/useToast';
import { ADMIN } from '../../providers/AccessProvider/permissions'; import useEnvironments from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import ConditionallyRender from 'component/common/ConditionallyRender';
import PageContent from 'component/common/PageContent/PageContent';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import HeaderTitle from 'component/common/HeaderTitle/HeaderTitle';
import { formatUnknownError } from 'utils/format-unknown-error';
const CreateEnvironment = () => { const CreateEnvironment = () => {
const { setToastApiError, setToastData } = useToast(); const { setToastApiError, setToastData } = useToast();
@ -49,8 +50,8 @@ const CreateEnvironment = () => {
confetti: true, confetti: true,
}); });
history.push('/environments'); history.push('/environments');
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -100,9 +101,7 @@ const CreateEnvironment = () => {
mode="Create" mode="Create"
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton permission={ADMIN} type="submit"> <CreateButton name="environment" permission={ADMIN} />
Create environment
</PermissionButton>
</EnvironmentForm> </EnvironmentForm>
</FormTemplate> </FormTemplate>
} }

View File

@ -1,14 +1,15 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useEnvironment from 'hooks/api/getters/useEnvironment/useEnvironment';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useEnvironment from '../../../hooks/api/getters/useEnvironment/useEnvironment';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../providers/AccessProvider/permissions'; import { ADMIN } from '../../providers/AccessProvider/permissions';
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm'; import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import useEnvironmentForm from '../hooks/useEnvironmentForm'; import useEnvironmentForm from '../hooks/useEnvironmentForm';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const EditEnvironment = () => { const EditEnvironment = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -49,8 +50,8 @@ const EditEnvironment = () => {
type: 'success', type: 'success',
title: 'Successfully updated environment.', title: 'Successfully updated environment.',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -85,9 +86,7 @@ const EditEnvironment = () => {
errors={errors} errors={errors}
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton permission={ADMIN} type="submit"> <UpdateButton permission={ADMIN} />
Edit environment
</PermissionButton>
</EnvironmentForm> </EnvironmentForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -1,10 +1,11 @@
import { import {
FormControl, FormControl,
FormControlLabel, FormControlLabel,
RadioGroup,
Radio, Radio,
RadioGroup,
} from '@material-ui/core'; } from '@material-ui/core';
import { useStyles } from './EnvironmentTypeSelector.styles'; import { useStyles } from './EnvironmentTypeSelector.styles';
import React from 'react';
interface IEnvironmentTypeSelectorProps { interface IEnvironmentTypeSelectorProps {
onChange: (event: React.FormEvent<HTMLInputElement>) => void; onChange: (event: React.FormEvent<HTMLInputElement>) => void;

View File

@ -11,7 +11,7 @@ interface IEnviromentDeleteConfirmProps {
open: boolean; open: boolean;
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>; setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>; setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteEnvironment: (name: string) => Promise<void>; handleDeleteEnvironment: () => Promise<void>;
confirmName: string; confirmName: string;
setConfirmName: React.Dispatch<React.SetStateAction<string>>; setConfirmName: React.Dispatch<React.SetStateAction<string>>;
} }

View File

@ -21,6 +21,7 @@ import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentTogg
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const EnvironmentList = () => { const EnvironmentList = () => {
const defaultEnv = { const defaultEnv = {
@ -75,16 +76,16 @@ const EnvironmentList = () => {
try { try {
await sortOrderAPICall(sortOrder); await sortOrderAPICall(sortOrder);
refetch(); refetch();
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => { const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
try { try {
await changeSortOrder(sortOrder); await changeSortOrder(sortOrder);
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -97,8 +98,8 @@ const EnvironmentList = () => {
title: 'Project environment deleted', title: 'Project environment deleted',
text: 'You have successfully deleted the project environment.', text: 'You have successfully deleted the project environment.',
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} finally { } finally {
setDeldialogue(false); setDeldialogue(false);
setSelectedEnv(defaultEnv); setSelectedEnv(defaultEnv);
@ -124,8 +125,8 @@ const EnvironmentList = () => {
title: 'Project environment enabled', title: 'Project environment enabled',
text: 'Your environment is enabled', text: 'Your environment is enabled',
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} finally { } finally {
refetch(); refetch();
} }
@ -140,8 +141,8 @@ const EnvironmentList = () => {
title: 'Project environment disabled', title: 'Project environment disabled',
text: 'Your environment is disabled.', text: 'Your environment is disabled.',
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} finally { } finally {
refetch(); refetch();
} }
@ -174,12 +175,11 @@ const EnvironmentList = () => {
<ResponsiveButton <ResponsiveButton
onClick={navigateToCreateEnvironment} onClick={navigateToCreateEnvironment}
maxWidth="700px" maxWidth="700px"
tooltip="Add environment"
Icon={Add} Icon={Add}
permission={ADMIN} permission={ADMIN}
disabled={!Boolean(uiConfig.flags.EEA)} disabled={!Boolean(uiConfig.flags.EEA)}
> >
Add Environment New Environment
</ResponsiveButton> </ResponsiveButton>
</> </>
} }

View File

@ -1,15 +1,16 @@
import FormTemplate from '../../common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm'; import FeatureForm from '../FeatureForm/FeatureForm';
import useFeatureForm from '../hooks/useFeatureForm'; import useFeatureForm from '../hooks/useFeatureForm';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast'; import useToast from 'hooks/useToast';
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions'; import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { CF_CREATE_BTN_ID } from '../../../testIds';
import { useContext } from 'react'; import { useContext } from 'react';
import UIContext from '../../../contexts/UIContext'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import UIContext from 'contexts/UIContext';
import { CF_CREATE_BTN_ID } from 'testIds';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const CreateFeature = () => { const CreateFeature = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -53,8 +54,8 @@ const CreateFeature = () => {
type: 'success', type: 'success',
}); });
setShowFeedback(true); setShowFeedback(true);
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} }
}; };
@ -99,15 +100,12 @@ const CreateFeature = () => {
mode="Create" mode="Create"
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton <CreateButton
onClick={handleSubmit} name="Feature"
permission={CREATE_FEATURE} permission={CREATE_FEATURE}
projectId={project} projectId={project}
type="submit"
data-test={CF_CREATE_BTN_ID} data-test={CF_CREATE_BTN_ID}
> />
Create toggle
</PermissionButton>
</FeatureForm> </FeatureForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -2,14 +2,15 @@ import FormTemplate from '../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm'; import FeatureForm from '../FeatureForm/FeatureForm';
import useFeatureForm from '../hooks/useFeatureForm'; import useFeatureForm from '../hooks/useFeatureForm';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
import { IFeatureViewParams } from '../../../interfaces/params';
import * as jsonpatch from 'fast-json-patch'; import * as jsonpatch from 'fast-json-patch';
import PermissionButton from '../../common/PermissionButton/PermissionButton'; import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions'; import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useFeature from 'hooks/api/getters/useFeature/useFeature';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { IFeatureViewParams } from 'interfaces/params';
import { formatUnknownError } from 'utils/format-unknown-error';
const EditFeature = () => { const EditFeature = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -57,8 +58,8 @@ const EditFeature = () => {
title: 'Toggle updated successfully', title: 'Toggle updated successfully',
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };
@ -101,13 +102,7 @@ const EditFeature = () => {
mode="Edit" mode="Edit"
clearErrors={clearErrors} clearErrors={clearErrors}
> >
<PermissionButton <UpdateButton permission={UPDATE_FEATURE} projectId={project} />
permission={UPDATE_FEATURE}
projectId={project}
type="submit"
>
Edit toggle
</PermissionButton>
</FeatureForm> </FeatureForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -183,7 +183,7 @@ const FeatureToggleList = ({
skeleton: loading, skeleton: loading,
})} })}
> >
Create feature toggle New feature toggle
</Button> </Button>
} }
/> />

View File

@ -165,7 +165,7 @@ exports[`renders correctly with one feature 1`] = `
<span <span
className="MuiButton-label" className="MuiButton-label"
> >
Create feature toggle New feature toggle
</span> </span>
</a> </a>
</div> </div>
@ -362,7 +362,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
<span <span
className="MuiButton-label" className="MuiButton-label"
> >
Create feature toggle New feature toggle
</span> </span>
</a> </a>
</div> </div>

View File

@ -1,9 +1,6 @@
import { Tooltip } from '@material-ui/core'; import { Tooltip } from '@material-ui/core';
import {
formatDateWithLocale,
formatFullDateTimeWithLocale,
} from '../../../common/util';
import { useLocationSettings } from '../../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../../hooks/useLocationSettings';
import { formatDateYMD, formatDateYMDHMS } from '../../../../utils/format-date';
interface CreatedAtProps { interface CreatedAtProps {
time: Date; time: Date;
@ -14,12 +11,12 @@ const CreatedAt = ({ time }: CreatedAtProps) => {
return ( return (
<Tooltip <Tooltip
title={`Created at ${formatFullDateTimeWithLocale( title={`Created at ${formatDateYMDHMS(
time, time,
locationSettings.locale locationSettings.locale
)}`} )}`}
> >
<span>{formatDateWithLocale(time, locationSettings.locale)}</span> <span>{formatDateYMD(time, locationSettings.locale)}</span>
</Tooltip> </Tooltip>
); );
}; };

View File

@ -1,10 +1,12 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { TableCell, TableRow } from '@material-ui/core'; import { TableCell, TableRow } from '@material-ui/core';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useStyles } from '../FeatureToggleListNew.styles'; import { useStyles } from '../FeatureToggleListNew.styles';
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv'; import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
import { IEnvironments } from '../../../../interfaces/featureToggle'; import {
IEnvironments,
IFeatureEnvironment,
} from '../../../../interfaces/featureToggle';
import useToast from '../../../../hooks/useToast'; import useToast from '../../../../hooks/useToast';
import { getTogglePath } from '../../../../utils/route-path-helpers'; import { getTogglePath } from '../../../../utils/route-path-helpers';
import { SyntheticEvent } from 'react-router/node_modules/@types/react'; import { SyntheticEvent } from 'react-router/node_modules/@types/react';
@ -25,8 +27,8 @@ interface IFeatureToggleListNewItemProps {
type: string; type: string;
environments: IFeatureEnvironment[]; environments: IFeatureEnvironment[];
projectId: string; projectId: string;
lastSeenAt?: Date; lastSeenAt?: string;
createdAt: Date; createdAt: string;
} }
const FeatureToggleListNewItem = ({ const FeatureToggleListNewItem = ({

View File

@ -1,9 +1,9 @@
import { ILocationSettings } from '../../../../../hooks/useLocationSettings'; import { ILocationSettings } from '../../../../../hooks/useLocationSettings';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { ChartOptions, defaults } from 'chart.js'; import { ChartOptions, defaults } from 'chart.js';
import { formatTimeWithLocale } from '../../../../common/util';
import { IFeatureMetricsRaw } from '../../../../../interfaces/featureToggle'; import { IFeatureMetricsRaw } from '../../../../../interfaces/featureToggle';
import theme from '../../../../../themes/main-theme'; import theme from '../../../../../themes/main-theme';
import { formatDateHM } from '../../../../../utils/format-date';
export const createChartOptions = ( export const createChartOptions = (
metrics: IFeatureMetricsRaw[], metrics: IFeatureMetricsRaw[],
@ -30,7 +30,7 @@ export const createChartOptions = (
usePointStyle: true, usePointStyle: true,
callbacks: { callbacks: {
title: items => title: items =>
formatTimeWithLocale( formatDateHM(
items[0].parsed.x, items[0].parsed.x,
locationSettings.locale locationSettings.locale
), ),
@ -73,10 +73,7 @@ export const createChartOptions = (
grid: { display: false }, grid: { display: false },
ticks: { ticks: {
callback: (_, i, data) => callback: (_, i, data) =>
formatTimeWithLocale( formatDateHM(data[i].value, locationSettings.locale),
data[i].value,
locationSettings.locale
),
}, },
}, },
}, },

View File

@ -9,8 +9,8 @@ import {
useTheme, useTheme,
} from '@material-ui/core'; } from '@material-ui/core';
import { useLocationSettings } from '../../../../../hooks/useLocationSettings'; import { useLocationSettings } from '../../../../../hooks/useLocationSettings';
import { formatFullDateTimeWithLocale } from '../../../../common/util';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { formatDateYMDHMS } from 'utils/format-date';
export const FEATURE_METRICS_TABLE_ID = 'feature-metrics-table-id'; export const FEATURE_METRICS_TABLE_ID = 'feature-metrics-table-id';
@ -48,7 +48,7 @@ export const FeatureMetricsTable = ({ metrics }: IFeatureMetricsTableProps) => {
{sortedMetrics.map(metric => ( {sortedMetrics.map(metric => (
<TableRow key={metric.timestamp}> <TableRow key={metric.timestamp}>
<TableCell> <TableCell>
{formatFullDateTimeWithLocale( {formatDateYMDHMS(
metric.timestamp, metric.timestamp,
locationSettings.locale locationSettings.locale
)} )}

View File

@ -1,6 +1,6 @@
import { DialogContentText } from '@material-ui/core'; import { DialogContentText } from '@material-ui/core';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useState } from 'react'; import React, { useState } from 'react';
import { IFeatureViewParams } from '../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../interfaces/params';
import Dialogue from '../../../../common/Dialogue'; import Dialogue from '../../../../common/Dialogue';
import Input from '../../../../common/Input/Input'; import Input from '../../../../common/Input/Input';
@ -11,6 +11,7 @@ import TagSelect from '../../../../common/TagSelect/TagSelect';
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useTags from '../../../../../hooks/api/getters/useTags/useTags'; import useTags from '../../../../../hooks/api/getters/useTags/useTags';
import useToast from '../../../../../hooks/useToast'; import useToast from '../../../../../hooks/useToast';
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
interface IAddTagDialogProps { interface IAddTagDialogProps {
open: boolean; open: boolean;
@ -20,6 +21,7 @@ interface IAddTagDialogProps {
interface IDefaultTag { interface IDefaultTag {
type: string; type: string;
value: string; value: string;
[index: string]: string; [index: string]: string;
} }
@ -62,9 +64,10 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
text: 'We successfully added a tag to your toggle', text: 'We successfully added a tag to your toggle',
confetti: true, confetti: true,
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); const message = formatUnknownError(error);
setErrors({ tagError: e.message }); setToastApiError(message);
setErrors({ tagError: message });
} }
}; };

View File

@ -8,6 +8,8 @@ import { IFeatureViewParams } from '../../../../../../interfaces/params';
import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch'; import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch';
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator'; import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
import { UPDATE_FEATURE_ENVIRONMENT } from '../../../../../providers/AccessProvider/permissions'; import { UPDATE_FEATURE_ENVIRONMENT } from '../../../../../providers/AccessProvider/permissions';
import React from 'react';
import { formatUnknownError } from '../../../../../../utils/format-unknown-error';
interface IFeatureOverviewEnvSwitchProps { interface IFeatureOverviewEnvSwitchProps {
env: IFeatureEnvironment; env: IFeatureEnvironment;
@ -40,7 +42,7 @@ const FeatureOverviewEnvSwitch = ({
if (callback) { if (callback) {
callback(); callback();
} }
} catch (e: any) { } catch (e) {
if (e.message === ENVIRONMENT_STRATEGY_ERROR) { if (e.message === ENVIRONMENT_STRATEGY_ERROR) {
showInfoBox(true); showInfoBox(true);
} else { } else {
@ -61,8 +63,8 @@ const FeatureOverviewEnvSwitch = ({
if (callback) { if (callback) {
callback(); callback();
} }
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };
@ -93,7 +95,6 @@ const FeatureOverviewEnvSwitch = ({
checked={env.enabled} checked={env.enabled}
onChange={toggleEnvironment} onChange={toggleEnvironment}
environmentId={env.name} environmentId={env.name}
tooltip={''}
/> />
{content} {content}
</div> </div>

View File

@ -1,5 +1,5 @@
import { Settings } from '@material-ui/icons'; import { Settings } from '@material-ui/icons';
import { useTheme } from '@material-ui/styles'; import { useTheme } from '@material-ui/core/styles';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../../../interfaces/params';
import { IFeatureStrategy } from '../../../../../../../../interfaces/strategy'; import { IFeatureStrategy } from '../../../../../../../../interfaces/strategy';

View File

@ -1,6 +1,6 @@
import { useState, useContext } from 'react'; import React, { useContext, useState } from 'react';
import { Chip } from '@material-ui/core'; import { Chip } from '@material-ui/core';
import { Label, Close } from '@material-ui/icons'; import { Close, Label } from '@material-ui/icons';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import useTags from '../../../../../../hooks/api/getters/useTags/useTags'; import useTags from '../../../../../../hooks/api/getters/useTags/useTags';
import { IFeatureViewParams } from '../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../interfaces/params';
@ -17,6 +17,7 @@ import useToast from '../../../../../../hooks/useToast';
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../../common/ConditionallyRender';
import AccessContext from '../../../../../../contexts/AccessContext'; import AccessContext from '../../../../../../contexts/AccessContext';
import { formatUnknownError } from '../../../../../../utils/format-unknown-error';
interface IFeatureOverviewTagsProps extends React.HTMLProps<HTMLButtonElement> { interface IFeatureOverviewTagsProps extends React.HTMLProps<HTMLButtonElement> {
projectId: string; projectId: string;
@ -53,8 +54,8 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
title: 'Tag deleted', title: 'Tag deleted',
text: 'Successfully deleted tag', text: 'Successfully deleted tag',
}); });
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useContext } from 'react'; import { useContext, useEffect, useState } from 'react';
import * as jsonpatch from 'fast-json-patch'; import * as jsonpatch from 'fast-json-patch';
import { TextField } from '@material-ui/core'; import { TextField } from '@material-ui/core';
import PermissionButton from '../../../../common/PermissionButton/PermissionButton'; import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
@ -11,6 +11,7 @@ import { IFeatureViewParams } from '../../../../../interfaces/params';
import useToast from '../../../../../hooks/useToast'; import useToast from '../../../../../hooks/useToast';
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
const FeatureSettingsMetadata = () => { const FeatureSettingsMetadata = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -54,8 +55,8 @@ const FeatureSettingsMetadata = () => {
}); });
setDirty(false); setDirty(false);
refetch(); refetch();
} catch (e) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useContext } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router'; import { useHistory, useParams } from 'react-router';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
@ -12,6 +12,7 @@ import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm'; import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
import { IPermission } from '../../../../../interfaces/user'; import { IPermission } from '../../../../../interfaces/user';
import { useAuthPermissions } from '../../../../../hooks/api/getters/useAuth/useAuthPermissions'; import { useAuthPermissions } from '../../../../../hooks/api/getters/useAuth/useAuthPermissions';
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
const FeatureSettingsProject = () => { const FeatureSettingsProject = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -61,16 +62,16 @@ const FeatureSettingsProject = () => {
history.replace( history.replace(
`/projects/${newProject}/features/${featureId}/settings` `/projects/${newProject}/features/${featureId}/settings`
); );
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };
const createMoveTargets = () => { const createMoveTargets = () => {
return permissions.reduce( return permissions.reduce(
(acc: { [key: string]: boolean }, permission: IPermission) => { (acc: { [key: string]: boolean }, p: IPermission) => {
if (permission.permission === MOVE_FEATURE_TOGGLE) { if (p.project && p.permission === MOVE_FEATURE_TOGGLE) {
acc[permission.project] = true; acc[p.project] = true;
} }
return acc; return acc;
}, },
@ -101,7 +102,6 @@ const FeatureSettingsProject = () => {
show={ show={
<PermissionButton <PermissionButton
permission={MOVE_FEATURE_TOGGLE} permission={MOVE_FEATURE_TOGGLE}
tooltip="Update feature"
onClick={() => setShowConfirmDialog(true)} onClick={() => setShowConfirmDialog(true)}
projectId={projectId} projectId={projectId}
> >

View File

@ -1,7 +1,8 @@
import { useStyles } from './FeatureStatus.styles'; import { useStyles } from './FeatureStatus.styles';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import ConditionallyRender from '../../../common/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender';
import { Tooltip } from '@material-ui/core'; import { Tooltip, TooltipProps } from '@material-ui/core';
import React from 'react';
function generateUnit(unit?: string): string { function generateUnit(unit?: string): string {
switch (unit) { switch (unit) {
@ -46,8 +47,8 @@ function getColor(unit?: string): string {
} }
interface FeatureStatusProps { interface FeatureStatusProps {
lastSeenAt?: Date; lastSeenAt?: string;
tooltipPlacement?: string; tooltipPlacement?: TooltipProps['placement'];
} }
const FeatureStatus = ({ const FeatureStatus = ({
@ -76,7 +77,7 @@ const FeatureStatus = ({
condition={!!lastSeenAt} condition={!!lastSeenAt}
show={ show={
<TimeAgo <TimeAgo
date={lastSeenAt} date={lastSeenAt!}
title="" title=""
live={false} live={false}
formatter={( formatter={(

View File

@ -19,6 +19,7 @@ import { ADD_NEW_STRATEGY_SAVE_ID } from '../../../../../../testIds';
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
import { scrollToTop } from '../../../../../common/util'; import { scrollToTop } from '../../../../../common/util';
import useToast from '../../../../../../hooks/useToast'; import useToast from '../../../../../../hooks/useToast';
import { formatUnknownError } from '../../../../../../utils/format-unknown-error';
const FeatureStrategiesConfigure = () => { const FeatureStrategiesConfigure = () => {
const history = useHistory(); const history = useHistory();
@ -99,8 +100,8 @@ const FeatureStrategiesConfigure = () => {
history.replace(history.location.pathname); history.replace(history.location.pathname);
refetch(); refetch();
scrollToTop(); scrollToTop();
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };

View File

@ -39,7 +39,6 @@ const FeatureStrategiesEnvironmentList = ({
const { const {
activeEnvironmentsRef, activeEnvironmentsRef,
setToastData,
deleteStrategy, deleteStrategy,
updateStrategy, updateStrategy,
delDialog, delDialog,
@ -162,7 +161,6 @@ const FeatureStrategiesEnvironmentList = ({
: 'Toggle is disabled and no strategies are executing' : 'Toggle is disabled and no strategies are executing'
} }
env={activeEnvironment} env={activeEnvironment}
setToastData={setToastData}
callback={updateFeatureEnvironmentCache} callback={updateFeatureEnvironmentCache}
/> />
</div> </div>

View File

@ -4,9 +4,13 @@ import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrate
import useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; import useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import useToast from '../../../../../../hooks/useToast'; import useToast from '../../../../../../hooks/useToast';
import { IFeatureViewParams } from '../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { IFeatureStrategy } from '../../../../../../interfaces/strategy'; import {
IFeatureStrategy,
IStrategyPayload,
} from '../../../../../../interfaces/strategy';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle'; import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
import { formatUnknownError } from '../../../../../../utils/format-unknown-error';
const useFeatureStrategiesEnvironmentList = () => { const useFeatureStrategiesEnvironmentList = () => {
const { projectId, featureId } = useParams<IFeatureViewParams>(); const { projectId, featureId } = useParams<IFeatureViewParams>();
@ -85,8 +89,8 @@ const useFeatureStrategiesEnvironmentList = () => {
strategy.constraints = updateStrategyPayload.constraints; strategy.constraints = updateStrategyPayload.constraints;
history.replace(history.location.pathname); history.replace(history.location.pathname);
setFeatureCache(feature); setFeatureCache(feature);
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };
@ -118,14 +122,13 @@ const useFeatureStrategiesEnvironmentList = () => {
text: `Successfully deleted strategy from ${featureId}`, text: `Successfully deleted strategy from ${featureId}`,
}); });
history.replace(history.location.pathname); history.replace(history.location.pathname);
} catch (e) { } catch (error: unknown) {
setToastApiError(e.message); setToastApiError(formatUnknownError(error));
} }
}; };
return { return {
activeEnvironmentsRef, activeEnvironmentsRef,
setToastData,
deleteStrategy, deleteStrategy,
updateStrategy, updateStrategy,
delDialog, delDialog,

View File

@ -282,7 +282,7 @@ const FeatureStrategiesEnvironments = () => {
environmentId={activeEnvironment.name} environmentId={activeEnvironment.name}
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
> >
Add new strategy New strategy
</ResponsiveButton> </ResponsiveButton>
} }
/> />

View File

@ -11,7 +11,7 @@ interface IFeatureStrategiesProductionGuard {
onClick: () => void; onClick: () => void;
onClose: () => void; onClose: () => void;
primaryButtonText: string; primaryButtonText: string;
loading: boolean; loading?: boolean;
} }
const FeatureStrategiesProductionGuard = ({ const FeatureStrategiesProductionGuard = ({
@ -61,4 +61,11 @@ const FeatureStrategiesProductionGuard = ({
); );
}; };
export const disableFeatureStrategiesProductionGuard = () => {
localStorage.setItem(
FEATURE_STRATEGY_PRODUCTION_GUARD_SETTING,
String(true)
);
};
export default FeatureStrategiesProductionGuard; export default FeatureStrategiesProductionGuard;

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { mutate } from 'swr'; import { mutate } from 'swr';
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext'; import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
@ -6,8 +6,8 @@ import useFeatureStrategy from '../../../../../../hooks/api/getters/useFeatureSt
import { IFeatureViewParams } from '../../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { import {
IConstraint, IConstraint,
IParameter,
IFeatureStrategy, IFeatureStrategy,
IParameter,
} from '../../../../../../interfaces/strategy'; } from '../../../../../../interfaces/strategy';
import FeatureStrategyAccordion from '../../FeatureStrategyAccordion/FeatureStrategyAccordion'; import FeatureStrategyAccordion from '../../FeatureStrategyAccordion/FeatureStrategyAccordion';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';

View File

@ -23,7 +23,7 @@ interface IFeatureStrategyCardProps {
name: string; name: string;
description: string; description: string;
configureNewStrategy: boolean; configureNewStrategy: boolean;
index?: number; index: number;
} }
export const FEATURE_STRATEGIES_DRAG_TYPE = 'FEATURE_STRATEGIES_DRAG_TYPE'; export const FEATURE_STRATEGIES_DRAG_TYPE = 'FEATURE_STRATEGIES_DRAG_TYPE';

View File

@ -8,7 +8,7 @@ import useStrategies from '../../../../../../hooks/api/getters/useStrategies/use
import GeneralStrategy from '../../common/GeneralStrategy/GeneralStrategy'; import GeneralStrategy from '../../common/GeneralStrategy/GeneralStrategy';
import UserWithIdStrategy from '../../common/UserWithIdStrategy/UserWithId'; import UserWithIdStrategy from '../../common/UserWithIdStrategy/UserWithId';
import StrategyConstraints from '../../common/StrategyConstraints/StrategyConstraints'; import StrategyConstraints from '../../common/StrategyConstraints/StrategyConstraints';
import { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import ConditionallyRender from '../../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../../common/ConditionallyRender';
import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig';
import { C } from '../../../../../common/flags'; import { C } from '../../../../../common/flags';

View File

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { import {
Switch,
FormControlLabel, FormControlLabel,
Tooltip, Switch,
TextField, TextField,
Tooltip,
} from '@material-ui/core'; } from '@material-ui/core';
import StrategyInputList from '../StrategyInputList/StrategyInputList'; import StrategyInputList from '../StrategyInputList/StrategyInputList';
import RolloutSlider from '../RolloutSlider/RolloutSlider'; import RolloutSlider from '../RolloutSlider/RolloutSlider';
import { import {
IParameter,
IFeatureStrategy, IFeatureStrategy,
IParameter,
} from '../../../../../../interfaces/strategy'; } from '../../../../../../interfaces/strategy';
import { useStyles } from './GeneralStrategy.styles'; import { useStyles } from './GeneralStrategy.styles';
@ -77,7 +77,7 @@ const GeneralStrategy = ({
</div> </div>
); );
} else if (type === 'list') { } else if (type === 'list') {
let list = []; let list: string[] = [];
if (typeof value === 'string') { if (typeof value === 'string') {
list = value.trim().split(',').filter(Boolean); list = value.trim().split(',').filter(Boolean);
} }

View File

@ -1,6 +1,7 @@
import { makeStyles, withStyles } from '@material-ui/core/styles'; import { makeStyles, withStyles } from '@material-ui/core/styles';
import { Slider, Typography } from '@material-ui/core'; import { Slider, Typography } from '@material-ui/core';
import { ROLLOUT_SLIDER_ID } from '../../../../../../testIds'; import { ROLLOUT_SLIDER_ID } from '../../../../../../testIds';
import React from 'react';
const StyledSlider = withStyles({ const StyledSlider = withStyles({
root: { root: {

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