1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

Merge branch 'main' into fix/empty-name-admin

This commit is contained in:
Youssef Khedher 2022-03-01 15:56:49 +01:00 committed by GitHub
commit c1e6860b8f
164 changed files with 1072 additions and 1490 deletions

View File

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

View File

@ -16,10 +16,10 @@ jobs:
uses: actions/checkout@v2
- name: Run Cypress
uses: cypress-io/github-action@v2
with:
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development"
with:
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }}
config: baseUrl=${{ github.event.deployment_status.target_url }}
record: true
spec: cypress/integration/feature-toggle/feature.spec.js
spec: cypress/integration/feature/feature.spec.ts
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]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:

View File

@ -1,21 +1,4 @@
# Developing
## Why did you render
This application is set up with [WDYR](https://github.com/welldone-software/why-did-you-render) and [craco](https://github.com/gsoft-inc/craco) in order to find, debug and remove uneccesary re-renders. This configuration can be found in /src/wdyr.ts.
In order to turn it on, change the configuration accordingly:
```
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
```
Now you should be able to review rendering information in the console. If you do utilise this functionality, please remember to set the configuration back to spare other developers the noise in the console.
# unleash-frontend
## Run with a local instance of the unleash-api:

View File

@ -1,37 +0,0 @@
const presetReact = require('@babel/preset-react').default;
const presetCRA = require('babel-preset-react-app');
module.exports = {
babel: {
loaderOptions: (babelLoaderOptions, { env, paths }) => {
const origBabelPresetReactAppIndex = babelLoaderOptions.presets.findIndex(
preset => {
return preset[0].includes('babel-preset-react-app');
}
);
if (origBabelPresetReactAppIndex === -1) {
return babelLoaderOptions;
}
const overridenBabelPresetReactApp = (...args) => {
const babelPresetReactAppResult = presetCRA(...args);
const origPresetReact = babelPresetReactAppResult.presets.find(
preset => {
return preset[0] === presetReact;
}
);
Object.assign(origPresetReact[1], {
importSource: '@welldone-software/why-did-you-render',
});
return babelPresetReactAppResult;
};
babelLoaderOptions.presets[
origBabelPresetReactAppIndex
] = overridenBabelPresetReactApp;
return babelLoaderOptions;
},
},
};

View File

@ -1,4 +1,6 @@
{
"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" />
// 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 password = 'qY70$NDcJNXA';

View File

@ -1,82 +1,48 @@
/* eslint-disable jest/no-conditional-expect */
/// <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 = '';
let enterprise = false;
import { disableFeatureStrategiesProductionGuard } from '../../../src/component/feature/FeatureView/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesProductionGuard/FeatureStrategiesProductionGuard';
const randomId = String(Math.random()).split('.')[1];
const featureToggleName = `unleash-e2e-${randomId}`;
const enterprise = Boolean(Cypress.env('ENTERPRISE'));
const passwordAuth = Cypress.env('PASSWORD_AUTH');
const authToken = Cypress.env('AUTH_TOKEN');
const baseUrl = Cypress.config().baseUrl;
let strategyId = '';
let defaultEnv = 'development';
describe('feature toggle', () => {
before(() => {
featureToggleName = `unleash-e2e-${Math.floor(Math.random() * 100)}`;
enterprise = Boolean(Cypress.env('ENTERPRISE'));
const env = Cypress.env('DEFAULT_ENV');
if (env) {
defaultEnv = env;
}
});
describe('feature', () => {
after(() => {
const authToken = Cypress.env('AUTH_TOKEN');
cy.request({
method: 'DELETE',
url: `${
Cypress.config().baseUrl
}/api/admin/features/${featureToggleName}`,
headers: {
Authorization: authToken,
},
url: `${baseUrl}/api/admin/features/${featureToggleName}`,
headers: { Authorization: authToken },
});
cy.request({
method: 'DELETE',
url: `${
Cypress.config().baseUrl
}/api/admin/archive/${featureToggleName}`,
headers: {
Authorization: authToken,
},
url: `${baseUrl}/api/admin/archive/${featureToggleName}`,
headers: { Authorization: authToken },
});
});
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// 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'));
disableFeatureStrategiesProductionGuard();
cy.visit('/');
if (passwordAuth) {
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_BUTTON']").click();
} else {
cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com');
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', () => {
if (
document.querySelectorAll("[data-test='CLOSE_SPLASH']").length > 0
) {
it('can create a feature toggle', () => {
if (document.querySelector("[data-test='CLOSE_SPLASH']")) {
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_DESC_ID'").type('hellowrdada');
cy.get("[data-test='CF_DESC_ID'").type('hello-world');
cy.get("[data-test='CF_CREATE_BTN_ID']").click();
cy.wait('@createFeature');
cy.url().should('include', featureToggleName);
});
it('Gives an error if a toggle exists with the same name', () => {
it('gives an error if a toggle exists with the same name', () => {
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
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_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='INPUT_ERROR_TEXT']").contains(
'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.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_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='INPUT_ERROR_TEXT']").contains(
`"name" must be URL friendly`
);
});
it('Can add a gradual rollout strategy to the development environment', () => {
cy.wait(1000);
it('can add a gradual rollout strategy to the development environment', () => {
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-2').click();
@ -147,10 +107,9 @@ describe('feature toggle', () => {
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`,
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
req => {
expect(req.body.name).to.equal('flexibleRollout');
expect(req.body.parameters.groupId).to.equal(featureToggleName);
expect(req.body.parameters.stickiness).to.equal('default');
expect(req.body.parameters.rollout).to.equal(30);
@ -172,7 +131,6 @@ describe('feature toggle', () => {
});
it('can update a strategy in the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=STRATEGY_ACCORDION_ID-flexibleRollout').click();
@ -188,7 +146,6 @@ describe('feature toggle', () => {
.first()
.click();
let newGroupId = 'new-group-id';
cy.get('[data-test=FLEXIBLE_STRATEGY_GROUP_ID]')
.first()
.clear()
@ -196,9 +153,9 @@ describe('feature toggle', () => {
cy.intercept(
'PUT',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`,
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
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.rollout).to.equal(60);
@ -219,12 +176,11 @@ describe('feature toggle', () => {
});
it('can delete a strategy in the development environment', () => {
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.intercept(
'DELETE',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies/${strategyId}`,
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
req => {
req.continue(res => {
expect(res.statusCode).to.equal(200);
@ -237,8 +193,7 @@ describe('feature toggle', () => {
cy.wait('@deleteStrategy');
});
it('Can add a userid strategy to the development environment', () => {
cy.wait(1000);
it('can add a userid strategy to the development environment', () => {
cy.visit(`/projects/default/features/${featureToggleName}/strategies`);
cy.get('[data-test=ADD_NEW_STRATEGY_ID]').click();
cy.get('[data-test=ADD_NEW_STRATEGY_CARD_BUTTON_ID-3').click();
@ -260,7 +215,7 @@ describe('feature toggle', () => {
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/${defaultEnv}/strategies`,
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
req => {
expect(req.body.name).to.equal('userWithId');
@ -282,11 +237,12 @@ describe('feature toggle', () => {
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 secondVariantName = 'my-second-variant';
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
cy.intercept(
'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/variants`,
@ -304,20 +260,23 @@ describe('feature toggle', () => {
expect(req.body[1].value.name).to.equal(secondVariantName);
}
}
).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);
).as('variantCreation');
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.get('[data-test=VARIANT_EDIT_BUTTON]').first().click();
cy.get('[data-test=VARIANT_NAME_INPUT]')
.children()
@ -328,6 +287,7 @@ describe('feature toggle', () => {
.find('input')
.check();
cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15');
cy.intercept(
'PATCH',
`/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].value).to.equal(150);
}
).as('variantupdate');
).as('variantUpdate');
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@variantupdate');
cy.wait('@variantUpdate');
cy.get('[data-test=VARIANT_WEIGHT]')
.first()
.should('have.text', '15 %');
});
it(`can delete variant`, () => {
it('can delete variant', () => {
const variantName = 'to-be-deleted';
cy.wait(1000);
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
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.intercept(
'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/variants`,
@ -365,6 +327,7 @@ describe('feature toggle', () => {
expect(e.path).to.match(/\//);
}
).as('delete');
cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click();
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
cy.wait('@delete');

View File

@ -1,7 +1,7 @@
{
"name": "unleash-frontend",
"description": "unleash your features",
"version": "4.8.0-beta.10",
"version": "4.8.0",
"keywords": [
"unleash",
"feature toggle",
@ -29,7 +29,6 @@
"start": "react-scripts start",
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
"start:ea": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
"start:demo": "UNLEASH_API=http://unleash.herokuapp.com yarn run start",
"test": "react-scripts test",
"prepare": "yarn run build",
"fmt": "prettier src --write --loglevel warn",
@ -42,6 +41,7 @@
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.60",
"@testing-library/dom": "8.11.3",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.3",
"@testing-library/user-event": "13.5.0",
@ -49,28 +49,23 @@
"@types/deep-diff": "1.0.1",
"@types/jest": "27.4.1",
"@types/lodash.clonedeep": "4.5.6",
"@types/node": "14.18.12",
"@types/node": "17.0.18",
"@types/react": "17.0.39",
"@types/react-dom": "17.0.11",
"@types/react-outside-click-handler": "1.3.1",
"@types/react-router-dom": "5.3.3",
"@types/react-test-renderer": "17.0.1",
"@types/react-timeago": "4.1.3",
"@welldone-software/why-did-you-render": "6.2.3",
"chart.js": "3.7.1",
"chartjs-adapter-date-fns": "2.0.0",
"classnames": "2.3.1",
"copy-to-clipboard": "3.3.1",
"craco": "0.0.3",
"css-loader": "6.6.0",
"cypress": "8.7.0",
"cypress": "9.5.1",
"date-fns": "2.28.0",
"debounce": "1.2.1",
"deep-diff": "1.0.2",
"fast-json-patch": "3.1.0",
"http-proxy-middleware": "2.0.3",
"lodash.clonedeep": "4.5.0",
"lodash.flow": "3.5.0",
"prettier": "2.5.1",
"prop-types": "15.8.1",
"react": "17.0.2",
@ -79,15 +74,13 @@
"react-dnd-html5-backend": "14.1.0",
"react-dom": "17.0.2",
"react-hooks-global-state": "1.0.2",
"react-outside-click-handler": "1.3.0",
"react-router-dom": "5.3.0",
"react-scripts": "4.0.3",
"react-test-renderer": "16.14.0",
"react-test-renderer": "17.0.2",
"react-timeago": "6.2.1",
"sass": "1.49.8",
"sass": "1.49.9",
"swr": "1.2.2",
"typescript": "4.5.5",
"web-vitals": "2.1.4"
"typescript": "4.6.2"
},
"jest": {
"moduleNameMapper": {
@ -117,6 +110,9 @@
"no-restricted-globals": "off",
"no-useless-computed-key": "off",
"import/no-anonymous-default-export": "off"
}
},
"ignorePatterns": [
"cypress"
]
}
}

View File

@ -7,7 +7,9 @@ import { IAddonProvider } from '../../../../interfaces/addons';
interface IAddonProps {
provider: IAddonProvider;
checkedEvents: string[];
setEventValue: (name: string) => void;
setEventValue: (
name: string
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
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 { TextField, FormControlLabel, Switch } from '@material-ui/core';
import { FormButtons, styles as commonStyles } from '../../common';
import { TextField, FormControlLabel, Switch, Button } from '@material-ui/core';
import { styles as commonStyles } from '../../common';
import { trim } from '../../common/util';
import { AddonParameters } from './AddonParameters/AddonParameters';
import { AddonEvents } from './AddonEvents/AddonEvents';
@ -79,7 +79,7 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
setErrors({ ...errors, events: undefined });
};
const handleCancel = () => {
const onCancel = () => {
history.goBack();
};
@ -203,10 +203,12 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
/>
</section>
<section className={styles.formSection}>
<FormButtons
submitText={submitText}
onCancel={handleCancel}
/>
<Button type="submit" color="primary" variant="contained">
{submitText}
</Button>
<Button type="button" onClick={onCancel}>
Cancel
</Button>
</section>
</form>
</PageContent>

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ import {
DELETE_API_TOKEN,
} from '../../../providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenList.styles';
import { formatDateWithLocale } from '../../../common/util';
import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons';
import Dialogue from '../../../common/Dialogue';
@ -32,6 +31,7 @@ import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
import { Alert } from '@material-ui/lab';
import copy from 'copy-to-clipboard';
import { useLocationSettings } from '../../../../hooks/useLocationSettings';
import { formatDateYMD } from '../../../../utils/format-date';
interface IApiToken {
createdAt: Date;
@ -146,7 +146,7 @@ export const ApiTokenList = () => {
align="left"
className={styles.hideSM}
>
{formatDateWithLocale(
{formatDateYMD(
item.createdAt,
locationSettings.locale
)}
@ -249,7 +249,7 @@ export const ApiTokenList = () => {
}
data-test={CREATE_API_TOKEN_BUTTON}
>
Create API token
New API token
</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 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 useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
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 { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { useState } from 'react';
import { scrollToTop } from '../../../common/util';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
export const CreateApiToken = () => {
const { setToastApiError } = useToast();
@ -49,8 +50,8 @@ export const CreateApiToken = () => {
setToken(api.secret);
setShowConfirm(true);
});
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
@ -95,9 +96,7 @@ export const CreateApiToken = () => {
mode="Create"
clearErrors={clearErrors}
>
<PermissionButton permission={ADMIN} type="submit">
Create token
</PermissionButton>
<CreateButton name="token" permission={ADMIN} />
</ApiTokenForm>
<ConfirmToken
open={showConfirm}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom';
import UserForm from '../UserForm/UserForm';
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 { useEffect } from 'react';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { EDIT } from '../../../../constants/misc';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
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 = () => {
useEffect(() => {
@ -60,8 +61,8 @@ const EditUser = () => {
title: 'User information updated',
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};
@ -94,9 +95,7 @@ const EditUser = () => {
clearErrors={clearErrors}
mode={EDIT}
>
<PermissionButton permission={ADMIN} type="submit">
Edit user
</PermissionButton>
<UpdateButton permission={ADMIN} />
</UserForm>
</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 => ({
userListBody: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,6 @@ const Constraint = ({
<div className={styles.btnContainer}>
<PermissionIconButton
onClick={editCallback}
tooltip="Edit strategy"
permission={UPDATE_FEATURE}
projectId={projectId}
>
@ -61,7 +60,6 @@ const Constraint = ({
<PermissionIconButton
onClick={deleteCallback}
tooltip="Delete strategy"
permission={UPDATE_FEATURE}
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 {
Button,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
Button,
DialogTitle,
} from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
@ -15,15 +15,15 @@ interface IDialogue {
primaryButtonText?: string;
secondaryButtonText?: string;
open: boolean;
onClick: (e: any) => void;
onClose: () => void;
onClick: (e: React.SyntheticEvent) => void;
onClose?: (e: React.SyntheticEvent) => void;
style?: object;
title: string;
fullWidth?: boolean;
maxWidth?: 'lg' | 'sm' | 'xs' | 'md' | 'xl';
disabledPrimaryButton?: boolean;
formId?: string;
permissionButton?: React.ReactNode;
permissionButton?: JSX.Element;
}
const Dialogue: React.FC<IDialogue> = ({
@ -69,7 +69,7 @@ const Dialogue: React.FC<IDialogue> = ({
<DialogActions>
<ConditionallyRender
condition={Boolean(permissionButton)}
show={permissionButton}
show={permissionButton!}
elseShow={
<ConditionallyRender
condition={Boolean(onClick)}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { TextField } from '@material-ui/core';
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> {
label: string;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
import { Switch, Tooltip } from '@material-ui/core';
import { OverridableComponent } from '@material-ui/core/OverridableComponent';
import { Switch, Tooltip, SwitchProps } from '@material-ui/core';
import AccessContext from '../../../contexts/AccessContext';
import React, { useContext } from 'react';
interface IPermissionSwitchProps extends OverridableComponent<any> {
interface IPermissionSwitchProps extends SwitchProps {
permission: string;
tooltip: string;
onChange?: (e: any) => void;
tooltip?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
projectId?: string;
environmentId?: string;
@ -19,7 +18,7 @@ const PermissionSwitch = React.forwardRef<
>((props, ref) => {
const {
permission,
tooltip = '',
tooltip,
disabled,
projectId,
environmentId,
@ -39,9 +38,9 @@ const PermissionSwitch = React.forwardRef<
access = hasAccess(permission);
}
const tooltipText = access
? tooltip
: "You don't have access to perform this operation";
const tooltipText = !access
? "You don't have access to perform this operation"
: '';
return (
<Tooltip title={tooltipText} arrow>

View File

@ -2,16 +2,18 @@ import { useMediaQuery } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender';
import PermissionButton from '../PermissionButton/PermissionButton';
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
import React from 'react';
interface IResponsiveButtonProps {
Icon: React.ElementType;
onClick: () => void;
tooltip?: string;
disabled?: boolean;
permission?: string;
permission: string;
projectId?: string;
environmentId?: string;
maxWidth: string;
className?: string;
}
const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({

View File

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

View File

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

View File

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

View File

@ -1,19 +1,19 @@
import { Portal } from '@material-ui/core';
import { useContext, useEffect } from 'react';
import { useCommonStyles } from '../../../common.styles';
import UIContext, { IToastData } from '../../../contexts/UIContext';
import UIContext from '../../../contexts/UIContext';
import { useStyles } from './ToastRenderer.styles';
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
import Toast from './Toast/Toast';
import { IToast } from '../../../interfaces/toast';
const ToastRenderer = () => {
// @ts-expect-error
const { toastData, setToast } = useContext(UIContext);
const commonStyles = useCommonStyles();
const styles = useStyles();
const hide = () => {
setToast((prev: IToastData) => ({ ...prev, show: false }));
setToast((prev: IToast) => ({ ...prev, show: false }));
};
useEffect(() => {
@ -31,7 +31,7 @@ const ToastRenderer = () => {
return (
<Portal>
<AnimateOnMount
mounted={toastData?.show}
mounted={Boolean(toastData?.show)}
start={commonStyles.fadeInBottomStartWithoutFixed}
enter={commonStyles.fadeInBottomEnter}
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 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 => {
if (r.flag && !flags[r.flag]) {
return false;
@ -32,27 +12,6 @@ export const scrollToTop = () => {
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 => {
if (value && 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 { useStyles } from './ContextForm.styles';
import React, { useState } from 'react';
import { Add } from '@material-ui/icons';
import { trim } from '../../common/util';
import { trim } from 'component/common/util';
interface IContextForm {
contextName: string;
@ -15,20 +15,20 @@ interface IContextForm {
setStickiness: React.Dispatch<React.SetStateAction<boolean>>;
setLegalValues: React.Dispatch<React.SetStateAction<string[]>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
onCancel: () => void;
errors: { [key: string]: string };
mode: string;
clearErrors: () => void;
validateNameUniqueness: () => void;
validateContext?: () => void;
setErrors: React.Dispatch<React.SetStateAction<Object>>;
}
const ENTER = 'Enter';
const ContextForm: React.FC<IContextForm> = ({
export const ContextForm: React.FC<IContextForm> = ({
children,
handleSubmit,
handleCancel,
onCancel,
contextName,
contextDesc,
legalValues,
@ -39,7 +39,7 @@ const ContextForm: React.FC<IContextForm> = ({
setStickiness,
errors,
mode,
validateNameUniqueness,
validateContext,
setErrors,
clearErrors,
}) => {
@ -108,7 +108,7 @@ const ContextForm: React.FC<IContextForm> = ({
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => clearErrors()}
onBlur={validateNameUniqueness}
onBlur={validateContext}
autoFocus
/>
<p className={styles.inputDescription}>
@ -187,12 +187,10 @@ const ContextForm: React.FC<IContextForm> = ({
</div>
<div className={styles.buttonContainer}>
{children}
<Button onClick={handleCancel} className={styles.cancelButton}>
<Button onClick={onCancel} className={styles.cancelButton}>
Cancel
</Button>
</div>
</form>
);
};
export default ContextForm;

View File

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

View File

@ -1,15 +1,16 @@
import { useHistory } from 'react-router-dom';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import useContextForm from '../hooks/useContextForm';
import ContextForm from '../ContextForm/ContextForm';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { CREATE_CONTEXT_FIELD } from '../../providers/AccessProvider/permissions';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
import useUnleashContext from '../../../hooks/api/getters/useUnleashContext/useUnleashContext';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useContextForm } from '../hooks/useContextForm';
import { ContextForm } from '../ContextForm/ContextForm';
import { CREATE_CONTEXT_FIELD } from 'component/providers/AccessProvider/permissions';
import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
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 { uiConfig } = useUiConfig();
const history = useHistory();
@ -23,8 +24,7 @@ const CreateContext = () => {
setLegalValues,
setStickiness,
getContextPayload,
validateNameUniqueness,
validateName,
validateContext,
clearErrors,
setErrors,
errors,
@ -34,7 +34,8 @@ const CreateContext = () => {
const handleSubmit = async (e: Event) => {
e.preventDefault();
const validName = validateName();
const validName = await validateContext();
if (validName) {
const payload = getContextPayload();
try {
@ -46,8 +47,8 @@ const CreateContext = () => {
confetti: true,
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};
@ -61,7 +62,7 @@ const CreateContext = () => {
--data-raw '${JSON.stringify(getContextPayload(), undefined, 2)}'`;
};
const handleCancel = () => {
const onCancel = () => {
history.goBack();
};
@ -69,7 +70,7 @@ const CreateContext = () => {
<FormTemplate
loading={loading}
title="Create context"
description="Context fields are a basic building block used in Unleash to control roll-out.
description="Context fields are a basic building block used in Unleash to control roll-out.
They can be used together with strategy constraints as part of the activation strategy evaluation."
documentationLink="https://docs.getunleash.io/how-to/how-to-define-custom-context-fields"
formatApiCode={formatApiCode}
@ -77,7 +78,7 @@ const CreateContext = () => {
<ContextForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
onCancel={onCancel}
contextName={contextName}
setContextName={setContextName}
contextDesc={contextDesc}
@ -87,19 +88,15 @@ const CreateContext = () => {
stickiness={stickiness}
setStickiness={setStickiness}
mode="Create"
validateNameUniqueness={validateNameUniqueness}
validateContext={validateContext}
setErrors={setErrors}
clearErrors={clearErrors}
>
<PermissionButton
<CreateButton
name="context"
permission={CREATE_CONTEXT_FIELD}
type="submit"
>
Create context
</PermissionButton>
/>
</ContextForm>
</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 { 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 { UPDATE_CONTEXT_FIELD } from '../../providers/AccessProvider/permissions';
import ContextForm from '../ContextForm/ContextForm';
import useContextForm from '../hooks/useContextForm';
import { formatUnknownError } from 'utils/format-unknown-error';
import { ContextForm } from '../ContextForm/ContextForm';
import { useContextForm } from '../hooks/useContextForm';
const EditContext = () => {
export const EditContext = () => {
useEffect(() => {
scrollToTop();
}, []);
@ -32,8 +33,6 @@ const EditContext = () => {
setLegalValues,
setStickiness,
getContextPayload,
validateNameUniqueness,
validateName,
clearErrors,
setErrors,
errors,
@ -56,24 +55,21 @@ const EditContext = () => {
const handleSubmit = async (e: Event) => {
e.preventDefault();
const payload = getContextPayload();
const validName = validateName();
if (validName) {
try {
await updateContext(payload);
refetch();
history.push('/context');
setToastData({
title: 'Context information updated',
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
}
try {
await updateContext(payload);
refetch();
history.push('/context');
setToastData({
title: 'Context information updated',
type: 'success',
});
} catch (e: unknown) {
setToastApiError(formatUnknownError(e));
}
};
const handleCancel = () => {
const onCancel = () => {
history.goBack();
};
@ -81,7 +77,7 @@ const EditContext = () => {
<FormTemplate
loading={loading}
title="Edit context"
description="Context fields are a basic building block used in Unleash to control roll-out.
description="Context fields are a basic building block used in Unleash to control roll-out.
They can be used together with strategy constraints as part of the activation strategy evaluation."
documentationLink="https://docs.getunleash.io/how-to/how-to-define-custom-context-fields"
formatApiCode={formatApiCode}
@ -89,7 +85,7 @@ const EditContext = () => {
<ContextForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
onCancel={onCancel}
contextName={contextName}
setContextName={setContextName}
contextDesc={contextDesc}
@ -99,19 +95,11 @@ const EditContext = () => {
stickiness={stickiness}
setStickiness={setStickiness}
mode="Edit"
validateNameUniqueness={validateNameUniqueness}
setErrors={setErrors}
clearErrors={clearErrors}
>
<PermissionButton
permission={UPDATE_CONTEXT_FIELD}
type="submit"
>
Edit context
</PermissionButton>
<UpdateButton permission={UPDATE_CONTEXT_FIELD} />
</ContextForm>
</FormTemplate>
);
};
export default EditContext;

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import useContextsApi from '../../../hooks/api/actions/useContextsApi/useContextsApi';
const useContextForm = (
export const useContextForm = (
initialcontextName = '',
initialcontextDesc = '',
initialLegalValues = [] as string[],
@ -42,25 +42,28 @@ const useContextForm = (
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 {
await validateContextName(contextName);
return true;
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
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 true;
};
const clearErrors = () => {
@ -77,12 +80,9 @@ const useContextForm = (
setLegalValues,
setStickiness,
getContextPayload,
validateNameUniqueness,
validateName,
validateContext,
setErrors,
clearErrors,
errors,
};
};
export default useContextForm;

View File

@ -1,19 +1,20 @@
import { useHistory } from 'react-router-dom';
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 FormTemplate from '../../common/FormTemplate/FormTemplate';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import { Alert } from '@material-ui/lab';
import { Button } from '@material-ui/core';
import ConditionallyRender from '../../common/ConditionallyRender';
import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import useEnvironments from 'hooks/api/getters/useEnvironments/useEnvironments';
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 { setToastApiError, setToastData } = useToast();
@ -49,8 +50,8 @@ const CreateEnvironment = () => {
confetti: true,
});
history.push('/environments');
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};
@ -100,9 +101,7 @@ const CreateEnvironment = () => {
mode="Create"
clearErrors={clearErrors}
>
<PermissionButton permission={ADMIN} type="submit">
Create environment
</PermissionButton>
<CreateButton name="environment" permission={ADMIN} />
</EnvironmentForm>
</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 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 EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import useEnvironmentForm from '../hooks/useEnvironmentForm';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const EditEnvironment = () => {
const { uiConfig } = useUiConfig();
@ -49,8 +50,8 @@ const EditEnvironment = () => {
type: 'success',
title: 'Successfully updated environment.',
});
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
@ -61,7 +62,7 @@ const EditEnvironment = () => {
return (
<FormTemplate
title="Edit environment"
description="Environments allow you to manage your
description="Environments allow you to manage your
product lifecycle from local development
through production. Your projects and
feature toggles are accessible in all your
@ -85,9 +86,7 @@ const EditEnvironment = () => {
errors={errors}
clearErrors={clearErrors}
>
<PermissionButton permission={ADMIN} type="submit">
Edit environment
</PermissionButton>
<UpdateButton permission={ADMIN} />
</EnvironmentForm>
</FormTemplate>
);

View File

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

View File

@ -11,7 +11,7 @@ interface IEnviromentDeleteConfirmProps {
open: boolean;
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteEnvironment: (name: string) => Promise<void>;
handleDeleteEnvironment: () => Promise<void>;
confirmName: 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 { ADMIN } from 'component/providers/AccessProvider/permissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const EnvironmentList = () => {
const defaultEnv = {
@ -75,16 +76,16 @@ const EnvironmentList = () => {
try {
await sortOrderAPICall(sortOrder);
refetch();
} catch (e) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
try {
await changeSortOrder(sortOrder);
} catch (e) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
@ -97,8 +98,8 @@ const EnvironmentList = () => {
title: 'Project environment deleted',
text: 'You have successfully deleted the project environment.',
});
} catch (e) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
} finally {
setDeldialogue(false);
setSelectedEnv(defaultEnv);
@ -124,8 +125,8 @@ const EnvironmentList = () => {
title: 'Project environment enabled',
text: 'Your environment is enabled',
});
} catch (e) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
} finally {
refetch();
}
@ -140,8 +141,8 @@ const EnvironmentList = () => {
title: 'Project environment disabled',
text: 'Your environment is disabled.',
});
} catch (e) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
} finally {
refetch();
}
@ -174,12 +175,11 @@ const EnvironmentList = () => {
<ResponsiveButton
onClick={navigateToCreateEnvironment}
maxWidth="700px"
tooltip="Add environment"
Icon={Add}
permission={ADMIN}
disabled={!Boolean(uiConfig.flags.EEA)}
>
Add Environment
New Environment
</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 FeatureForm from '../FeatureForm/FeatureForm';
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 { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { CF_CREATE_BTN_ID } from '../../../testIds';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
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 { setToastData, setToastApiError } = useToast();
@ -53,8 +54,8 @@ const CreateFeature = () => {
type: 'success',
});
setShowFeedback(true);
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};
@ -99,15 +100,12 @@ const CreateFeature = () => {
mode="Create"
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
<CreateButton
name="Feature"
permission={CREATE_FEATURE}
projectId={project}
type="submit"
data-test={CF_CREATE_BTN_ID}
>
Create toggle
</PermissionButton>
/>
</FeatureForm>
</FormTemplate>
);

View File

@ -2,14 +2,15 @@ import FormTemplate from '../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm';
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 PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
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 { setToastData, setToastApiError } = useToast();
@ -57,8 +58,8 @@ const EditFeature = () => {
title: 'Toggle updated successfully',
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
@ -101,13 +102,7 @@ const EditFeature = () => {
mode="Edit"
clearErrors={clearErrors}
>
<PermissionButton
permission={UPDATE_FEATURE}
projectId={project}
type="submit"
>
Edit toggle
</PermissionButton>
<UpdateButton permission={UPDATE_FEATURE} projectId={project} />
</FeatureForm>
</FormTemplate>
);

View File

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

View File

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

View File

@ -135,7 +135,7 @@ const FeatureToggleListNew = ({
type={feature.type}
environments={feature.environments}
projectId={projectId}
createdAt={new Date()}
createdAt={new Date().toISOString()}
/>
);
});

View File

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

View File

@ -1,13 +1,11 @@
import { useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { TableCell, TableRow } from '@material-ui/core';
import { useHistory } from 'react-router';
import { useStyles } from '../FeatureToggleListNew.styles';
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
import { IEnvironments } from '../../../../interfaces/featureToggle';
import useToast from '../../../../hooks/useToast';
import { getTogglePath } from '../../../../utils/route-path-helpers';
import { SyntheticEvent } from 'react-router/node_modules/@types/react';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import FeatureStatus from '../../FeatureView/FeatureStatus/FeatureStatus';
import FeatureType from '../../FeatureView/FeatureType/FeatureType';
@ -23,10 +21,10 @@ import EnvironmentStrategyDialog from '../../../common/EnvironmentStrategiesDial
interface IFeatureToggleListNewItemProps {
name: string;
type: string;
environments: IFeatureEnvironment[];
environments: IEnvironments[];
projectId: string;
lastSeenAt?: Date;
createdAt: Date;
lastSeenAt?: string;
createdAt: string;
}
const FeatureToggleListNewItem = ({
@ -47,7 +45,7 @@ const FeatureToggleListNewItem = ({
const { refetch } = useProject(projectId);
const styles = useStyles();
const history = useHistory();
const ref = useRef(null);
const ref = useRef<HTMLButtonElement>(null);
const [showInfoBox, setShowInfoBox] = useState(false);
const [environmentName, setEnvironmentName] = useState('');
@ -55,8 +53,8 @@ const FeatureToggleListNewItem = ({
setShowInfoBox(false);
};
const onClick = (e: SyntheticEvent) => {
if (!ref.current?.contains(e.target)) {
const onClick = (e: React.MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
history.push(getTogglePath(projectId, name));
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import useFeatureTypes from '../../../../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
import GeneralSelect from '../../../../../common/GeneralSelect/GeneralSelect';
import GeneralSelect, {
ISelectOption,
} from '../../../../../common/GeneralSelect/GeneralSelect';
const FeatureTypeSelect = ({
editable,
@ -11,7 +13,7 @@ const FeatureTypeSelect = ({
}) => {
const { featureTypes } = useFeatureTypes();
const options = featureTypes.map(t => ({
const options: ISelectOption[] = featureTypes.map(t => ({
key: t.id,
label: t.name,
title: t.description,

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ interface IFeatureStrategiesProductionGuard {
onClick: () => void;
onClose: () => void;
primaryButtonText: string;
loading: boolean;
loading?: boolean;
}
const FeatureStrategiesProductionGuard = ({
@ -61,4 +61,11 @@ const FeatureStrategiesProductionGuard = ({
);
};
export const disableFeatureStrategiesProductionGuard = () => {
localStorage.setItem(
FEATURE_STRATEGY_PRODUCTION_GUARD_SETTING,
String(true)
);
};
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 { mutate } from 'swr';
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
@ -6,8 +6,8 @@ import useFeatureStrategy from '../../../../../../hooks/api/getters/useFeatureSt
import { IFeatureViewParams } from '../../../../../../interfaces/params';
import {
IConstraint,
IParameter,
IFeatureStrategy,
IParameter,
} from '../../../../../../interfaces/strategy';
import FeatureStrategyAccordion from '../../FeatureStrategyAccordion/FeatureStrategyAccordion';
import cloneDeep from 'lodash.clonedeep';

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