1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-19 17:52:45 +02:00

feat: biome lint frontend (#4903)

Follows up on https://github.com/Unleash/unleash/pull/4853 to add Biome
to the frontend as well.


![image](https://github.com/Unleash/unleash/assets/14320932/1906faf1-fc29-4172-a4d4-b2716d72cd65)

Added a few `biome-ignore` to speed up the process but we may want to
check and fix them in the future.
This commit is contained in:
Nuno Góis 2023-10-02 13:25:46 +01:00 committed by GitHub
parent 751bc465d6
commit 4167a60588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
920 changed files with 5755 additions and 7835 deletions

1
.gitignore vendored
View File

@ -36,7 +36,6 @@ unleash-server.tar.gz
# Visual Studio Code
jsconfig.json
typings
.vscode
.nyc_output
# We use yarn.lock

View File

@ -1,2 +0,0 @@
CHANGELOG.md
website/docs

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.defaultFormatter": "biomejs.biome"
}

View File

@ -41,8 +41,8 @@
"website/translated_docs",
"website",
"setupJest.js",
"frontend",
"dist",
"build",
"src/migrations/*.js",
"src/test/examples/*.json",
"website/**/*.js",
@ -67,16 +67,13 @@
"website/translated_docs",
"website",
"setupJest.js",
"frontend",
"dist",
"build",
"src/migrations/*.js",
"src/migrations/*.json",
"src/test/examples/*.json",
"website/**/*.js",
"coverage",
".eslintrc",
".eslintignore",
"package.json"
"coverage"
],
"indentSize": 4
},

View File

@ -1,2 +0,0 @@
.github/*
CHANGELOG.md

View File

@ -1,7 +0,0 @@
{
"singleQuote": true,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"printWidth": 80
}

View File

@ -15,7 +15,7 @@ export default defineConfig({
vitePreprocessor({
configFile: path.resolve(__dirname, './vite.config.ts'),
mode: 'development',
})
}),
);
on('task', {
log(message) {

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {}
type Chainable = {};
}

View File

@ -26,25 +26,25 @@ declare namespace Cypress {
createProject_UI(
projectName: string,
defaultStickiness: string
defaultStickiness: string,
): Chainable;
createFeature_UI(
name: string,
shouldWait?: boolean,
project?: string
project?: string,
): Chainable;
// VARIANTS
addVariantsToFeature_UI(
featureToggleName: string,
variants: Array<string>,
projectName?: string
projectName?: string,
);
deleteVariant_UI(
featureToggleName: string,
variant: string,
projectName?: string
projectName?: string,
): Chainable<any>;
// SEGMENTS
@ -54,16 +54,16 @@ declare namespace Cypress {
// STRATEGY
addUserIdStrategyToFeature_UI(
featureName: string,
projectName?: string
projectName?: string,
): Chainable;
addFlexibleRolloutStrategyToFeature_UI(
options: AddFlexibleRolloutStrategyOptions
options: AddFlexibleRolloutStrategyOptions,
): Chainable;
updateFlexibleRolloutStrategy_UI(featureToggleName: string);
deleteFeatureStrategy_UI(
featureName: string,
shouldWait?: boolean,
projectName?: string
projectName?: string,
): Chainable;
// API
@ -72,22 +72,22 @@ declare namespace Cypress {
addUserToProject_API(
id: number,
role: number,
projectName?: string
projectName?: string,
): Chainable;
createProject_API(
name: string,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable;
deleteProject_API(name: string): Chainable;
createFeature_API(
name: string,
projectName?: string,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable;
deleteFeature_API(name: string): Chainable;
createEnvironment_API(
environment: IEnvironment,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable;
}
}

View File

@ -16,7 +16,7 @@ describe('demo', () => {
name: 'dev',
type: 'development',
},
optionsIgnore409
optionsIgnore409,
);
cy.createProject_API('demo-app', optionsIgnore409);
cy.createFeature_API('demoApp.step1', 'demo-app', optionsIgnore409);
@ -32,10 +32,10 @@ describe('demo', () => {
cy.get("[data-testid='CLOSE_SPLASH']").click();
}
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, req => {
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, (req) => {
req.headers['cache-control'] =
'no-cache, no-store, must-revalidate';
req.on('response', res => {
req.on('response', (res) => {
if (res.body) {
res.body.flags = {
...res.body.flags,
@ -93,7 +93,7 @@ describe('demo', () => {
'log',
`Testing topic #${topic + 1} "${
currentTopic.title
}", step #${step + 1}...`
}", step #${step + 1}...`,
);
if (!currentStep.optional) {

View File

@ -28,14 +28,14 @@ describe('feature', () => {
it('gives an error if a toggle exists with the same name', () => {
cy.createFeature_UI(featureToggleName, false);
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
'A toggle with that name already exists'
'A toggle with that name already exists',
);
});
it('gives an error if a toggle name is url unsafe', () => {
cy.createFeature_UI('featureToggleUnsafe####$#//', false);
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
`"name" must be URL friendly`
`"name" must be URL friendly`,
);
});
@ -44,7 +44,7 @@ describe('feature', () => {
featureToggleName,
}).then(() => {
cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() =>
cy.deleteFeatureStrategy_UI(featureToggleName)
cy.deleteFeatureStrategy_UI(featureToggleName),
);
});
});
@ -71,7 +71,7 @@ describe('feature', () => {
cy.intercept(
'PATCH',
`/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`,
req => {
(req) => {
expect(req.body[0].op).to.equal('replace');
expect(req.body[0].path).to.equal('/1/weightType');
expect(req.body[0].value).to.equal('fix');
@ -81,13 +81,13 @@ describe('feature', () => {
expect(req.body[2].op).to.equal('replace');
expect(req.body[2].path).to.equal('/0/weight');
expect(req.body[2].value).to.equal(850);
}
},
).as('variantUpdate');
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
cy.get(`[data-testid=VARIANT_WEIGHT_${variant2}]`).should(
'have.text',
'15 %'
'15 %',
);
});

View File

@ -15,13 +15,13 @@ describe('groups', () => {
email: `unleash-e2e-user${i}-${randomId}@test.com`,
sendEmail: false,
rootRole: 3,
}).then(response => userIds.push(response.body.id));
}).then((response) => userIds.push(response.body.id));
}
});
after(() => {
userIds.forEach(id =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`)
userIds.forEach((id) =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`),
);
});
@ -55,7 +55,7 @@ describe('groups', () => {
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
cy.get("[data-testid='INPUT_ERROR_TEXT'").contains(
'A group with that name already exists.'
'A group with that name already exists.',
);
});

View File

@ -15,13 +15,13 @@ describe('imports', () => {
email: `unleash-e2e-user${i}-${randomFeatureName}@test.com`,
sendEmail: false,
rootRole: 3,
}).then(response => userIds.push(response.body.id));
}).then((response) => userIds.push(response.body.id));
}
});
after(() => {
userIds.forEach(id =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`)
userIds.forEach((id) =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`),
);
});
@ -118,13 +118,13 @@ describe('imports', () => {
cy.wait(500);
cy.get(
"[data-testid='feature-toggle-status'] input[type='checkbox']:checked"
"[data-testid='feature-toggle-status'] input[type='checkbox']:checked",
)
.invoke('attr', 'aria-label')
.should('eq', 'development');
cy.get(
'[data-testid="FEATURE_ENVIRONMENT_ACCORDION_development"]'
'[data-testid="FEATURE_ENVIRONMENT_ACCORDION_development"]',
).click();
cy.contains('50%');
});

View File

@ -31,13 +31,13 @@ describe('project-access', () => {
rootRole: 3,
})
.as(name)
.then(response => {
.then((response) => {
const id = response.body.id;
userIds.push(id);
cy.request('POST', `${baseUrl}/api/admin/groups`, {
name: `${i}-${groupAndProjectName}`,
users: [{ user: { id: id } }],
}).then(response => {
}).then((response) => {
const id = response.body.id;
groupIds.push(id);
});
@ -50,26 +50,26 @@ describe('project-access', () => {
});
after(() => {
userIds.forEach(id =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`)
userIds.forEach((id) =>
cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`),
);
groupIds.forEach(id =>
cy.request('DELETE', `${baseUrl}/api/admin/groups/${id}`)
groupIds.forEach((id) =>
cy.request('DELETE', `${baseUrl}/api/admin/groups/${id}`),
);
cy.request(
'DELETE',
`${baseUrl}/api/admin/projects/${groupAndProjectName}`
`${baseUrl}/api/admin/projects/${groupAndProjectName}`,
);
});
beforeEach(() => {
cy.login_UI();
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, req => {
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, (req) => {
req.headers['cache-control'] =
'no-cache, no-store, must-revalidate';
req.on('response', res => {
req.on('response', (res) => {
if (res.body) {
res.body.flags = {
...res.body.flags,
@ -90,7 +90,7 @@ describe('project-access', () => {
cy.intercept(
'POST',
`/api/admin/projects/${groupAndProjectName}/access`
`/api/admin/projects/${groupAndProjectName}/access`,
).as('assignAccess');
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
@ -109,7 +109,7 @@ describe('project-access', () => {
cy.intercept(
'POST',
`/api/admin/projects/${groupAndProjectName}/access`
`/api/admin/projects/${groupAndProjectName}/access`,
).as('assignAccess');
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
@ -128,7 +128,7 @@ describe('project-access', () => {
cy.intercept(
'PUT',
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`,
).as('editAccess');
cy.get(`[data-testid='CancelIcon']`).last().click();
@ -148,7 +148,7 @@ describe('project-access', () => {
cy.intercept(
'PUT',
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`,
).as('editAccess');
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
@ -167,7 +167,7 @@ describe('project-access', () => {
cy.intercept(
'DELETE',
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`,
).as('removeAccess');
cy.contains("Yes, I'm sure").click();

View File

@ -27,7 +27,7 @@ describe('segments', () => {
cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName);
cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").should('be.disabled');
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
'Segment name already exists'
'Segment name already exists',
);
});

View File

@ -2,12 +2,12 @@
import Chainable = Cypress.Chainable;
const baseUrl = Cypress.config().baseUrl;
const password = Cypress.env(`AUTH_PASSWORD`) + '_A';
const password = `${Cypress.env(`AUTH_PASSWORD`)}_A`;
const PROJECT_MEMBER = 5;
export const createFeature_API = (
featureName: string,
projectName?: string,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable<any> => {
const project = projectName || 'default';
return cy.request({
@ -36,7 +36,7 @@ export const deleteFeature_API = (name: string): Chainable<any> => {
export const createProject_API = (
project: string,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable<any> => {
return cy.request({
url: `${baseUrl}/api/admin/projects`,
@ -71,10 +71,10 @@ export const createUser_API = (userName: string, role: number) => {
rootRole: role,
})
.as(name)
.then(response => {
.then((response) => {
const id = response.body.id;
updateUserPassword_API(id).then(() => {
addUserToProject_API(id, PROJECT_MEMBER).then(value => {
addUserToProject_API(id, PROJECT_MEMBER).then((value) => {
userIds.push(id);
userCredentials.push({ email, password });
});
@ -89,13 +89,13 @@ export const updateUserPassword_API = (id: number, pass?: string): Chainable =>
`${baseUrl}/api/admin/user-admin/${id}/change-password`,
{
password: pass || password,
}
},
);
export const addUserToProject_API = (
id: number,
role: number,
projectName?: string
projectName?: string,
): Chainable => {
const project = projectName || 'default';
return cy.request(
@ -104,7 +104,7 @@ export const addUserToProject_API = (
{
groups: [],
users: [{ id }],
}
},
);
};
@ -115,7 +115,7 @@ interface IEnvironment {
export const createEnvironment_API = (
environment: IEnvironment,
options?: Partial<Cypress.RequestOptions>
options?: Partial<Cypress.RequestOptions>,
): Chainable<any> => {
return cy.request({
url: `${baseUrl}/api/admin/environments`,

View File

@ -15,7 +15,7 @@ const disableActiveSplashScreens = () => {
const disableFeatureStrategiesProdGuard = () => {
localStorage.setItem(
'useFeatureStrategyProdGuardSettings:v2',
JSON.stringify({ hide: true })
JSON.stringify({ hide: true }),
);
};
@ -26,7 +26,7 @@ export const runBefore = () => {
export const login_UI = (
user = AUTH_USER,
password = AUTH_PASSWORD
password = AUTH_PASSWORD,
): Chainable<any> => {
return cy.session(user, () => {
cy.visit('/');
@ -51,14 +51,14 @@ export const login_UI = (
export const createFeature_UI = (
name: string,
shouldWait?: boolean,
project?: string
project?: string,
): Chainable<any> => {
const projectName = project || 'default';
cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as(
'createFeature'
'createFeature',
);
cy.wait(300);
@ -72,7 +72,7 @@ export const createFeature_UI = (
export const createProject_UI = (
projectName: string,
defaultStickiness: string
defaultStickiness: string,
): Chainable<any> => {
cy.get('[data-testid=NAVIGATE_TO_CREATE_PROJECT').click();
@ -111,7 +111,7 @@ export const deleteSegment_UI = (segmentName: string): Chainable<any> => {
};
export const addFlexibleRolloutStrategyToFeature_UI = (
options: AddStrategyOptions
options: AddStrategyOptions,
): Chainable<any> => {
const { featureToggleName, project, environment, stickiness } = options;
const projectName = project || 'default';
@ -123,7 +123,7 @@ export const addFlexibleRolloutStrategyToFeature_UI = (
cy.intercept(
'POST',
`/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/strategies`,
req => {
(req) => {
expect(req.body.name).to.equal('flexibleRollout');
expect(req.body.parameters.groupId).to.equal(featureToggleName);
expect(req.body.parameters.stickiness).to.equal(defaultStickiness);
@ -135,14 +135,14 @@ export const addFlexibleRolloutStrategyToFeature_UI = (
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
req.continue((res) => {
strategyId = res.body.id;
});
}
},
).as('addStrategyToFeature');
cy.visit(
`/projects/${projectName}/features/${featureToggleName}/strategies/create?environmentId=${env}&strategyName=flexibleRollout`
`/projects/${projectName}/features/${featureToggleName}/strategies/create?environmentId=${env}&strategyName=flexibleRollout`,
);
cy.wait(500);
// Takes a bit to load the screen - this will wait until it finds it or fail
@ -157,11 +157,11 @@ export const addFlexibleRolloutStrategyToFeature_UI = (
export const updateFlexibleRolloutStrategy_UI = (
featureToggleName: string,
projectName?: string
projectName?: string,
) => {
const project = projectName || 'default';
cy.visit(
`/projects/${project}/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}`
`/projects/${project}/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}`,
);
cy.wait(500);
@ -182,7 +182,7 @@ export const updateFlexibleRolloutStrategy_UI = (
cy.intercept(
'PUT',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
req => {
(req) => {
expect(req.body.parameters.groupId).to.equal('new-group-id');
expect(req.body.parameters.stickiness).to.equal('sessionId');
expect(req.body.parameters.rollout).to.equal('50');
@ -193,10 +193,10 @@ export const updateFlexibleRolloutStrategy_UI = (
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
req.continue((res) => {
expect(res.statusCode).to.equal(200);
});
}
},
).as('updateStrategy');
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
@ -206,18 +206,18 @@ export const updateFlexibleRolloutStrategy_UI = (
export const deleteFeatureStrategy_UI = (
featureToggleName: string,
shouldWait?: boolean,
projectName?: string
projectName?: string,
): Chainable<any> => {
const project = projectName || 'default';
cy.intercept(
'DELETE',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
req => {
req.continue(res => {
(req) => {
req.continue((res) => {
expect(res.statusCode).to.equal(200);
});
}
},
).as('deleteUserStrategy');
cy.visit(`/projects/${project}/features/${featureToggleName}`);
cy.get('[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]').click();
@ -230,11 +230,11 @@ export const deleteFeatureStrategy_UI = (
export const addUserIdStrategyToFeature_UI = (
featureToggleName: string,
projectName: string
projectName: string,
): Chainable<any> => {
const project = projectName || 'default';
cy.visit(
`/projects/${project}/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId`
`/projects/${project}/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=userWithId`,
);
if (ENTERPRISE) {
@ -255,7 +255,7 @@ export const addUserIdStrategyToFeature_UI = (
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
req => {
(req) => {
expect(req.body.name).to.equal('userWithId');
expect(req.body.parameters.userIds.length).to.equal(11);
@ -266,10 +266,10 @@ export const addUserIdStrategyToFeature_UI = (
expect(req.body.constraints.length).to.equal(0);
}
req.continue(res => {
req.continue((res) => {
strategyId = res.body.id;
});
}
},
).as('addStrategyToFeature');
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
@ -279,7 +279,7 @@ export const addUserIdStrategyToFeature_UI = (
export const addVariantsToFeature_UI = (
featureToggleName: string,
variants: Array<string>,
projectName: string
projectName: string,
) => {
const project = projectName || 'default';
cy.visit(`/projects/${project}/features/${featureToggleName}/variants`);
@ -287,16 +287,16 @@ export const addVariantsToFeature_UI = (
cy.intercept(
'PATCH',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`,
req => {
(req) => {
variants.forEach((variant, index) => {
expect(req.body[index].op).to.equal('add');
expect(req.body[index].path).to.equal(`/${index}`);
expect(req.body[index].value.name).to.equal(variant);
expect(req.body[index].value.weight).to.equal(
1000 / variants.length
1000 / variants.length,
);
});
}
},
).as('variantCreation');
cy.get('[data-testid=ADD_VARIANT_BUTTON]').first().click();
@ -314,7 +314,7 @@ export const addVariantsToFeature_UI = (
export const deleteVariant_UI = (
featureToggleName: string,
variant: string,
projectName?: string
projectName?: string,
): Chainable<any> => {
const project = projectName || 'default';
cy.visit(`/projects/${project}/features/${featureToggleName}/variants`);
@ -325,13 +325,13 @@ export const deleteVariant_UI = (
cy.intercept(
'PATCH',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`,
req => {
(req) => {
expect(req.body[0].op).to.equal('remove');
expect(req.body[0].path).to.equal('/1');
expect(req.body[1].op).to.equal('replace');
expect(req.body[1].path).to.equal('/0/weight');
expect(req.body[1].value).to.equal(1000);
}
},
).as('delete');
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();

View File

@ -47,14 +47,14 @@ Cypress.Commands.add('deleteVariant_UI', deleteVariant_UI);
Cypress.Commands.add('addVariantsToFeature_UI', addVariantsToFeature_UI);
Cypress.Commands.add(
'addUserIdStrategyToFeature_UI',
addUserIdStrategyToFeature_UI
addUserIdStrategyToFeature_UI,
);
Cypress.Commands.add(
'addFlexibleRolloutStrategyToFeature_UI',
addFlexibleRolloutStrategyToFeature_UI
addFlexibleRolloutStrategyToFeature_UI,
);
Cypress.Commands.add(
'updateFlexibleRolloutStrategy_UI',
updateFlexibleRolloutStrategy_UI
updateFlexibleRolloutStrategy_UI,
);
Cypress.Commands.add('createEnvironment_API', createEnvironment_API);

View File

@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
// require('./commands')

View File

@ -5,5 +5,5 @@ const { version } = module.exports;
module.exports = {
publicFolder: path.join(__dirname, 'build'),
version
version,
};

View File

@ -2,10 +2,7 @@
"name": "unleash-frontend-local",
"version": "0.0.0",
"private": true,
"files": [
"index.js",
"build"
],
"files": ["index.js", "build"],
"engines": {
"node": ">=18"
},
@ -21,10 +18,10 @@
"test": "tsc && NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest run",
"test:snapshot": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn test -u",
"test:watch": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest watch",
"lint": "eslint --fix ./src",
"lint:check": "eslint ./src",
"fmt": "prettier src --write --loglevel warn",
"fmt:check": "prettier src --check",
"lint": "biome lint src --apply",
"lint:check": "biome lint src",
"fmt": "biome format src --write",
"fmt:check": "biome format src",
"ts:check": "tsc",
"e2e": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn run cypress open --config baseUrl='http://localhost:3000' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all",
"e2e:heroku": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn run cypress open --config baseUrl='https://unleash.herokuapp.com' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all",
@ -33,6 +30,7 @@
"gen:api:sandbox": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" UNLEASH_OPENAPI_URL=https://sandbox.getunleash.io/demo2/docs/openapi.json yarn run gen:api"
},
"devDependencies": {
"@biomejs/biome": "^1.2.2",
"@codemirror/lang-json": "6.0.1",
"@emotion/react": "11.11.1",
"@emotion/styled": "11.11.0",
@ -75,8 +73,6 @@
"debounce": "1.2.1",
"deep-diff": "1.0.2",
"dequal": "2.0.3",
"eslint": "8.50.0",
"eslint-config-react-app": "7.0.1",
"fast-json-patch": "3.1.1",
"http-proxy-middleware": "2.0.6",
"immer": "9.0.21",
@ -88,7 +84,6 @@
"msw": "0.49.3",
"pkginfo": "0.4.1",
"plausible-tracker": "0.3.8",
"prettier": "2.8.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-chartjs-2": "4.3.1",
@ -136,34 +131,12 @@
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"parserOptions": {
"warnOnUnsupportedTypeScriptVersion": false
},
"rules": {
"no-restricted-globals": "off",
"no-useless-computed-key": "off",
"import/no-anonymous-default-export": "off",
"react-hooks/exhaustive-deps": "off"
},
"ignorePatterns": [
"cypress"
]
},
"dependencies": {}
}

View File

@ -1,7 +1,7 @@
import { Suspense, useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { Error } from 'component/layout/Error/Error';
import { Error as LayoutError } from 'component/layout/Error/Error';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeedbackNPS } from 'component/feedback/FeedbackNPS/FeedbackNPS';
import { LayoutPicker } from 'component/layout/LayoutPicker/LayoutPicker';
@ -37,7 +37,7 @@ export const App = () => {
const { isOss, uiConfig } = useUiConfig();
const availableRoutes = isOss()
? routes.filter(route => !route.enterprise)
? routes.filter((route) => !route.enterprise)
: routes;
useEffect(() => {
@ -47,9 +47,9 @@ export const App = () => {
}, [authDetails, user]);
return (
<ErrorBoundary FallbackComponent={Error}>
<ErrorBoundary FallbackComponent={LayoutError}>
<PlausibleProvider>
<ErrorBoundary FallbackComponent={Error}>
<ErrorBoundary FallbackComponent={LayoutError}>
<SWRProvider>
<Suspense fallback={<Loader />}>
<ConditionallyRender
@ -59,46 +59,48 @@ export const App = () => {
<>
<ConditionallyRender
condition={Boolean(
uiConfig?.maintenanceMode
uiConfig?.maintenanceMode,
)}
show={<MaintenanceBanner />}
/>
<StyledContainer>
<ToastRenderer />
<Routes>
{availableRoutes.map(route => (
<Route
key={route.path}
path={route.path}
element={
<LayoutPicker
isStandalone={
route.isStandalone ===
true
}
>
<ProtectedRoute
route={
route
{availableRoutes.map(
(route) => (
<Route
key={route.path}
path={route.path}
element={
<LayoutPicker
isStandalone={
route.isStandalone ===
true
}
/>
</LayoutPicker>
}
/>
))}
>
<ProtectedRoute
route={
route
}
/>
</LayoutPicker>
}
/>
),
)}
<Route
path="/"
path='/'
element={
<InitialRedirect />
}
/>
<Route
path="*"
path='*'
element={<NotFound />}
/>
</Routes>
<FeedbackNPS openUrl="http://feedback.unleash.run" />
<FeedbackNPS openUrl='http://feedback.unleash.run' />
<SplashPageRedirect />
</StyledContainer>

View File

@ -25,26 +25,26 @@ export const Admin = () => {
<AdminTabsMenu />
<Routes>
<Route index element={<AdminIndex />} />
<Route path="users/*" element={<UsersAdmin />} />
<Route path="api" element={<ApiTokenPage />} />
<Route path="api/create-token" element={<CreateApiToken />} />
<Route path="service-accounts" element={<ServiceAccounts />} />
<Route path="create-user" element={<CreateUser />} />
<Route path="invite-link" element={<InviteLink />} />
<Route path="groups/*" element={<GroupsAdmin />} />
<Route path="roles/*" element={<Roles />} />
<Route path="instance" element={<InstanceAdmin />} />
<Route path="network/*" element={<Network />} />
<Route path="maintenance" element={<MaintenanceAdmin />} />
<Route path="cors" element={<CorsAdmin />} />
<Route path="auth" element={<AuthSettings />} />
<Route path='users/*' element={<UsersAdmin />} />
<Route path='api' element={<ApiTokenPage />} />
<Route path='api/create-token' element={<CreateApiToken />} />
<Route path='service-accounts' element={<ServiceAccounts />} />
<Route path='create-user' element={<CreateUser />} />
<Route path='invite-link' element={<InviteLink />} />
<Route path='groups/*' element={<GroupsAdmin />} />
<Route path='roles/*' element={<Roles />} />
<Route path='instance' element={<InstanceAdmin />} />
<Route path='network/*' element={<Network />} />
<Route path='maintenance' element={<MaintenanceAdmin />} />
<Route path='cors' element={<CorsAdmin />} />
<Route path='auth' element={<AuthSettings />} />
<Route
path="admin-invoices"
path='admin-invoices'
element={<FlaggedBillingRedirect />}
/>
<Route path="billing" element={<Billing />} />
<Route path="instance-privacy" element={<InstancePrivacy />} />
<Route path="*" element={<NotFound />} />
<Route path='billing' element={<Billing />} />
<Route path='instance-privacy' element={<InstancePrivacy />} />
<Route path='*' element={<NotFound />} />
</Routes>
</>
);

View File

@ -10,35 +10,42 @@ import { useAdminRoutes } from './useAdminRoutes';
export const AdminIndex: VFC = () => {
const adminRoutes = useAdminRoutes();
const routeGroups = adminRoutes.reduce((acc, route) => {
const group = route.group || 'other';
const routeGroups = adminRoutes.reduce(
(acc, route) => {
const group = route.group || 'other';
const index = acc.findIndex(item => item.name === group);
if (index === -1) {
acc.push({
name: group,
description: adminGroups[group] || 'Other',
items: [route],
});
const index = acc.findIndex((item) => item.name === group);
if (index === -1) {
acc.push({
name: group,
description: adminGroups[group] || 'Other',
items: [route],
});
return acc;
}
acc[index].items.push(route);
return acc;
}
acc[index].items.push(route);
return acc;
}, [] as Array<{ name: string; description: string; items: INavigationMenuItem[] }>);
},
[] as Array<{
name: string;
description: string;
items: INavigationMenuItem[];
}>,
);
return (
<PageContent header={<PageHeader title="Manage Unleash" />}>
{routeGroups.map(group => (
<PageContent header={<PageHeader title='Manage Unleash' />}>
{routeGroups.map((group) => (
<Box
key={group.name}
sx={theme => ({ marginBottom: theme.spacing(2) })}
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
>
<Typography variant="h2">{group.description}</Typography>
<Typography variant='h2'>{group.description}</Typography>
<ul>
{group.items.map(route => (
{group.items.map((route) => (
<li key={route.path}>
<Link component={RouterLink} to={route.path}>
{route.title}

View File

@ -1,3 +1,3 @@
import { Navigate } from 'react-router-dom';
export const AdminRedirect = () => <Navigate to="/admin/users" replace />;
export const AdminRedirect = () => <Navigate to='/admin/users' replace />;

View File

@ -5,13 +5,13 @@ export const ApiTokenDocs = () => {
const { uiConfig } = useUiConfig();
return (
<Alert severity="info">
<Alert severity='info'>
<p>
Read the{' '}
<a
href="https://docs.getunleash.io/reference/sdks"
target="_blank"
rel="noreferrer"
href='https://docs.getunleash.io/reference/sdks'
target='_blank'
rel='noreferrer'
>
SDK overview
</a>{' '}

View File

@ -26,9 +26,9 @@ const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
<ConditionallyRender
condition={isUnleashCloud}
show={
<Alert severity="info" sx={{ mb: 4 }}>
<Alert severity='info' sx={{ mb: 4 }}>
Please be aware of our{' '}
<Link href="https://www.getunleash.io/fair-use-policy">
<Link href='https://www.getunleash.io/fair-use-policy'>
fair use policy
</Link>
.

View File

@ -21,10 +21,10 @@ export const EnvironmentSelector = ({
const selectableEnvs =
type === TokenType.ADMIN
? [{ key: '*', label: 'ALL' }]
: environments.map(environment => ({
: environments.map((environment) => ({
key: environment.name,
label: `${environment.name.concat(
!environment.enabled ? ' - deprecated' : ''
!environment.enabled ? ' - deprecated' : '',
)}`,
title: environment.name,
disabled: false,
@ -40,9 +40,9 @@ export const EnvironmentSelector = ({
options={selectableEnvs}
value={environment}
onChange={setEnvironment}
label="Environment"
id="api_key_environment"
name="environment"
label='Environment'
id='api_key_environment'
name='environment'
IconComponent={KeyboardArrowDownOutlined}
fullWidth
/>

View File

@ -23,7 +23,7 @@ export const ProjectSelector = ({
const projectId = useOptionalPathParam('projectId');
const { projects: availableProjects } = useProjects();
const selectableProjects = availableProjects.map(project => ({
const selectableProjects = availableProjects.map((project) => ({
value: project.id,
label: project.name,
}));

View File

@ -17,7 +17,7 @@ export const SelectAllButton: FC<SelectAllButtonProps> = ({
}) => {
return (
<Box sx={{ ml: 3.5, my: 0.5 }}>
<StyledLink onClick={onClick} component="button" underline="hover">
<StyledLink onClick={onClick} component='button' underline='hover'>
{isAllSelected ? 'Deselect all' : 'Select all'}
</StyledLink>
</Box>

View File

@ -36,7 +36,7 @@ describe('SelectProjectInput', () => {
render(<SelectProjectInput {...mockProps} />);
const checkbox = screen.getByLabelText(
/all current and future projects/i
/all current and future projects/i,
);
expect(checkbox).toBeChecked();
@ -52,7 +52,7 @@ describe('SelectProjectInput', () => {
await user.click(screen.getByTestId('select-all-projects'));
expect(
screen.getByLabelText(/all current and future projects/i)
screen.getByLabelText(/all current and future projects/i),
).not.toBeChecked();
expect(screen.getByLabelText('Projects')).toBeEnabled();
@ -60,7 +60,7 @@ describe('SelectProjectInput', () => {
await user.click(screen.getByTestId('select-all-projects'));
expect(
screen.getByLabelText(/all current and future projects/i)
screen.getByLabelText(/all current and future projects/i),
).toBeChecked();
expect(screen.getByLabelText('Projects')).toBeDisabled();
@ -68,11 +68,11 @@ describe('SelectProjectInput', () => {
it('renders with autocomplete enabled if default value is not a wildcard', () => {
render(
<SelectProjectInput {...mockProps} defaultValue={['project1']} />
<SelectProjectInput {...mockProps} defaultValue={['project1']} />,
);
const checkbox = screen.getByLabelText(
/all current and future projects/i
/all current and future projects/i,
);
expect(checkbox).not.toBeChecked();
@ -117,7 +117,7 @@ describe('SelectProjectInput', () => {
{ label: 'Project1', value: 'project1' },
{ label: 'Project2', value: 'project2' },
]}
/>
/>,
);
await user.click(screen.getByLabelText('Projects'));
@ -140,7 +140,7 @@ describe('SelectProjectInput', () => {
{ label: 'Charlie', value: 'charlie' },
{ label: 'Alpaca', value: 'alpaca' },
]}
/>
/>,
);
const input = await screen.findByLabelText('Projects');
await user.type(input, 'alp');

View File

@ -48,10 +48,10 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
onFocus,
}) => {
const [projects, setProjects] = useState<string[]>(
typeof defaultValue === 'string' ? [defaultValue] : defaultValue
typeof defaultValue === 'string' ? [defaultValue] : defaultValue,
);
const [isWildcardSelected, selectWildcard] = useState(
typeof defaultValue === 'string' || defaultValue.includes(ALL_PROJECTS)
typeof defaultValue === 'string' || defaultValue.includes(ALL_PROJECTS),
);
const isAllSelected =
projects.length > 0 &&
@ -60,7 +60,7 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
const onAllProjectsChange = (
e: ChangeEvent<HTMLInputElement>,
checked: boolean
checked: boolean,
) => {
if (checked) {
selectWildcard(true);
@ -82,12 +82,12 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
const renderOption = (
props: object,
option: IAutocompleteBoxOption,
{ selected }: AutocompleteRenderOptionState
{ selected }: AutocompleteRenderOptionState,
) => (
<li {...props}>
<SelectOptionCheckbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
checkedIcon={<CheckBoxIcon fontSize='small' />}
checked={selected}
/>
{option.label}
@ -114,11 +114,11 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
{...params}
error={Boolean(error)}
helperText={error}
variant="outlined"
label="Projects"
placeholder="Select one or more projects"
variant='outlined'
label='Projects'
placeholder='Select one or more projects'
onFocus={onFocus}
data-testid="select-input"
data-testid='select-input'
/>
);
@ -127,14 +127,14 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
<Box sx={{ mt: 1, mb: 0.25, ml: 1.5 }}>
<FormControlLabel
disabled={disabled}
data-testid="select-all-projects"
data-testid='select-all-projects'
control={
<Checkbox
checked={disabled || isWildcardSelected}
onChange={onAllProjectsChange}
/>
}
label="ALL current and future projects"
label='ALL current and future projects'
/>
</Box>
<Autocomplete
@ -153,8 +153,8 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
value={
isWildcardSelected || disabled
? options
: options.filter(option =>
projects.includes(option.value)
: options.filter((option) =>
projects.includes(option.value),
)
}
onChange={(_, input) => {

View File

@ -22,9 +22,9 @@ export const TokenInfo = ({
</StyledInputDescription>
<StyledInput
value={username}
name="username"
onChange={e => setUsername(e.target.value)}
label="Token name"
name='username'
onChange={(e) => setUsername(e.target.value)}
label='Token name'
error={errors.username !== undefined}
errorText={errors.username}
onFocus={() => clearErrors('username')}

View File

@ -29,13 +29,13 @@ export const TokenTypeSelector = ({
return (
<StyledContainer>
<FormControl sx={{ mb: 2, width: '100%' }}>
<StyledInputLabel id="token-type">
<StyledInputLabel id='token-type'>
What do you want to connect?
</StyledInputLabel>
<RadioGroup
aria-labelledby="token-type"
defaultValue="CLIENT"
name="radio-buttons-group"
aria-labelledby='token-type'
defaultValue='CLIENT'
name='radio-buttons-group'
value={type}
onChange={(_, value) => setType(value as TokenType)}
>
@ -59,8 +59,8 @@ export const TokenTypeSelector = ({
<Box>
<Typography>{label}</Typography>
<Typography
variant="body2"
color="text.secondary"
variant='body2'
color='text.secondary'
>
{title}
</Typography>
@ -68,7 +68,7 @@ export const TokenTypeSelector = ({
</Box>
}
/>
)
),
)}
</RadioGroup>
</FormControl>

View File

@ -16,12 +16,12 @@ export type ApiTokenFormErrorType = 'username' | 'projects';
export const useApiTokenForm = (project?: string) => {
const { environments } = useEnvironments();
const { uiConfig } = useUiConfig();
const initialEnvironment = environments?.find(e => e.enabled)?.name;
const initialEnvironment = environments?.find((e) => e.enabled)?.name;
const hasCreateTokenPermission = useHasRootAccess(CREATE_CLIENT_API_TOKEN);
const hasCreateProjectTokenPermission = useHasRootAccess(
CREATE_PROJECT_API_TOKEN,
project
project,
);
const apiTokenTypes: SelectOption[] = [
@ -38,7 +38,7 @@ export const useApiTokenForm = (project?: string) => {
const hasCreateFrontendAccess = useHasRootAccess(CREATE_FRONTEND_API_TOKEN);
const hasCreateFrontendTokenAccess = useHasRootAccess(
CREATE_PROJECT_API_TOKEN,
project
project,
);
if (!project) {
apiTokenTypes.push({
@ -58,7 +58,7 @@ export const useApiTokenForm = (project?: string) => {
});
}
const firstAccessibleType = apiTokenTypes.find(t => t.enabled)?.key;
const firstAccessibleType = apiTokenTypes.find((t) => t.enabled)?.key;
const [username, setUsername] = useState('');
const [type, setType] = useState(firstAccessibleType || TokenType.CLIENT);
@ -99,10 +99,10 @@ export const useApiTokenForm = (project?: string) => {
const isValid = () => {
const newErrors: Partial<Record<ApiTokenFormErrorType, string>> = {};
if (!username) {
newErrors['username'] = 'Username is required';
newErrors.username = 'Username is required';
}
if (projects.length === 0) {
newErrors['projects'] = 'At least one project is required';
newErrors.projects = 'At least one project is required';
}
setErrors(newErrors);

View File

@ -34,7 +34,7 @@ export const ApiTokenPage = () => {
setGlobalFilter,
setHiddenColumns,
columns,
} = useApiTokenTable(tokens, props => {
} = useApiTokenTable(tokens, (props) => {
const READ_PERMISSION =
props.row.original.type === 'client'
? READ_CLIENT_API_TOKEN
@ -91,7 +91,7 @@ export const ApiTokenPage = () => {
CREATE_CLIENT_API_TOKEN,
ADMIN,
]}
path="/admin/api/create-token"
path='/admin/api/create-token'
/>
</>
}

View File

@ -25,21 +25,21 @@ export const ConfirmToken = ({
open={open}
setOpen={setOpen}
onClick={closeConfirm}
primaryButtonText="Close"
title="New token created"
primaryButtonText='Close'
title='New token created'
>
<Typography variant="body1">
<Typography variant='body1'>
Your new token has been created successfully.
</Typography>
<UserToken token={token} />
<ConditionallyRender
condition={type === TokenType.FRONTEND}
show={
<Alert sx={{ mt: 2 }} severity="info">
<Alert sx={{ mt: 2 }} severity='info'>
By default, all {TokenType.FRONTEND} tokens may be used
from any CORS origin. If you'd like to configure a
strict set of origins, please use the{' '}
<Link to="/admin/cors" target="_blank" rel="noreferrer">
<Link to='/admin/cors' target='_blank' rel='noreferrer'>
CORS origins configuration page
</Link>
.

View File

@ -25,7 +25,7 @@ export const UserToken = ({ token }: IUserTokenProps) => {
return (
<Box
sx={theme => ({
sx={(theme) => ({
backgroundColor: theme.palette.background.elevation2,
padding: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px`,
@ -37,8 +37,8 @@ export const UserToken = ({ token }: IUserTokenProps) => {
})}
>
{token}
<Tooltip title="Copy token" arrow>
<IconButton onClick={copyToken} size="large">
<Tooltip title='Copy token' arrow>
<IconButton onClick={copyToken} size='large'>
<CopyIcon />
</IconButton>
</Tooltip>

View File

@ -66,8 +66,8 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
const payload = getApiTokenPayload();
await createToken(payload)
.then(res => res.json())
.then(api => {
.then((res) => res.json())
.then((api) => {
scrollToTop();
setToken(api.secret);
setShowConfirm(true);
@ -84,9 +84,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/${PATH}' \\
return `curl --location --request POST '${uiConfig.unleashUrl}/${PATH}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getApiTokenPayload(), undefined, 2)}'`;
@ -102,17 +100,17 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
title={pageTitle}
modal={modal}
description="Unleash SDKs use API tokens to authenticate to the Unleash API. Client SDKs need a token with 'client privileges', which allows them to fetch feature toggle configurations and post usage metrics."
documentationLink="https://docs.getunleash.io/reference/api-tokens-and-client-keys"
documentationLinkLabel="API tokens documentation"
documentationLink='https://docs.getunleash.io/reference/api-tokens-and-client-keys'
documentationLinkLabel='API tokens documentation'
formatApiCode={formatApiCode}
>
<ApiTokenForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Create"
mode='Create'
actions={
<CreateButton
name="token"
name='token'
permission={[
ADMIN,
CREATE_CLIENT_API_TOKEN,

View File

@ -7,9 +7,9 @@ describe('ProjectsList', () => {
it('should prioritize new "projects" array over deprecated "project"', async () => {
render(
<ProjectsList
project="project"
project='project'
projects={['project1', 'project2']}
/>
/>,
);
const links = await screen.findAllByRole('link');
@ -21,7 +21,7 @@ describe('ProjectsList', () => {
});
it('should render correctly with single "project"', async () => {
render(<ProjectsList project="project" />);
render(<ProjectsList project='project' />);
const links = await screen.findAllByRole('link');
expect(links).toHaveLength(1);

View File

@ -23,7 +23,7 @@ export const ProjectsList: VFC<IProjectsListProps> = ({
project,
}) => {
const { searchQuery } = useSearchHighlightContext();
let fields: string[] =
const fields: string[] =
projects && Array.isArray(projects)
? projects
: project

View File

@ -34,7 +34,7 @@ export const AuthSettings = () => {
component: <GoogleAuth />,
},
].filter(
item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google'
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
);
const [activeTab, setActiveTab] = useState(0);
@ -52,8 +52,8 @@ export const AuthSettings = () => {
onChange={(_, tabId) => {
setActiveTab(tabId);
}}
indicatorColor="primary"
textColor="primary"
indicatorColor='primary'
textColor='primary'
>
{tabs.map((tab, index) => (
<Tab
@ -75,12 +75,12 @@ export const AuthSettings = () => {
>
<ConditionallyRender
condition={authenticationType === 'open-source'}
show={<PremiumFeature feature="sso" />}
show={<PremiumFeature feature='sso' />}
/>
<ConditionallyRender
condition={authenticationType === 'demo'}
show={
<Alert severity="warning">
<Alert severity='warning'>
You are running Unleash in demo mode. You have
to use the Enterprise edition in order configure
Single Sign-on.
@ -90,7 +90,7 @@ export const AuthSettings = () => {
<ConditionallyRender
condition={authenticationType === 'custom'}
show={
<Alert severity="warning">
<Alert severity='warning'>
You have decided to use custom authentication
type. You have to use the Enterprise edition in
order configure Single Sign-on from the user
@ -101,7 +101,7 @@ export const AuthSettings = () => {
<ConditionallyRender
condition={authenticationType === 'hosted'}
show={
<Alert severity="info">
<Alert severity='info'>
Your Unleash instance is managed by the Unleash
team.
</Alert>
@ -113,6 +113,7 @@ export const AuthSettings = () => {
<div>
{tabs.map((tab, index) => (
<TabPanel
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={index}
value={activeTab}
index={index}

View File

@ -51,12 +51,12 @@ export const AutoCreateForm = ({
control={
<Switch
onChange={updateAutoCreate}
name="enabled"
name='enabled'
checked={data.autoCreate}
disabled={!data.enabled}
/>
}
label="Auto-create users"
label='Auto-create users'
/>
</Grid>
</Grid>
@ -70,22 +70,22 @@ export const AutoCreateForm = ({
</Grid>
<Grid item md={6}>
<FormControl style={{ minWidth: '200px' }}>
<InputLabel id="defaultRootRole-label">
<InputLabel id='defaultRootRole-label'>
Default Role
</InputLabel>
<Select
label="Default Role"
labelId="defaultRootRole-label"
id="defaultRootRole"
name="defaultRootRole"
label='Default Role'
labelId='defaultRootRole-label'
id='defaultRootRole'
name='defaultRootRole'
disabled={!data.autoCreate || !data.enabled}
value={data.defaultRootRole || 'Editor'}
onChange={updateDefaultRootRole}
>
{/*consider these from API or constants. */}
<MenuItem value="Viewer">Viewer</MenuItem>
<MenuItem value="Editor">Editor</MenuItem>
<MenuItem value="Admin">Admin</MenuItem>
<MenuItem value='Viewer'>Viewer</MenuItem>
<MenuItem value='Editor'>Editor</MenuItem>
<MenuItem value='Admin'>Admin</MenuItem>
</Select>
</FormControl>
</Grid>
@ -101,16 +101,16 @@ export const AutoCreateForm = ({
<Grid item md={6}>
<TextField
onChange={updateField}
label="Email domains"
name="emailDomains"
label='Email domains'
name='emailDomains'
disabled={!data.autoCreate || !data.enabled}
required={Boolean(data.autoCreate)}
value={data.emailDomains || ''}
placeholder="@company.com, @anotherCompany.com"
placeholder='@company.com, @anotherCompany.com'
style={{ width: '400px' }}
rows={2}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>

View File

@ -69,17 +69,17 @@ export const GoogleAuth = () => {
return (
<>
<Box>
<Alert severity="error" sx={{ mb: 2 }}>
<Alert severity='error' sx={{ mb: 2 }}>
This integration is deprecated and will be removed in next
major version. Please use <strong>OpenID Connect</strong> to
enable Google SSO.
</Alert>
<Alert severity="info" sx={{ mb: 3 }}>
<Alert severity='info' sx={{ mb: 3 }}>
Read the{' '}
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication/google"
target="_blank"
rel="noreferrer"
href='https://www.unleash-hosted.com/docs/enterprise-authentication/google'
target='_blank'
rel='noreferrer'
>
documentation
</a>{' '}
@ -103,7 +103,7 @@ export const GoogleAuth = () => {
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
name='enabled'
checked={data.enabled}
/>
}
@ -122,13 +122,13 @@ export const GoogleAuth = () => {
<Grid item xs={6}>
<TextField
onChange={updateField}
label="Client ID"
name="clientId"
placeholder=""
label='Client ID'
name='clientId'
placeholder=''
value={data.clientId}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -144,13 +144,13 @@ export const GoogleAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Client Secret"
name="clientSecret"
label='Client Secret'
name='clientSecret'
value={data.clientSecret}
placeholder=""
placeholder=''
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -172,13 +172,13 @@ export const GoogleAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Unleash Hostname"
name="unleashHostname"
placeholder=""
label='Unleash Hostname'
name='unleashHostname'
placeholder=''
value={data.unleashHostname || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
@ -193,7 +193,7 @@ export const GoogleAuth = () => {
<Grid item md={6} style={{ padding: '20px' }}>
<Switch
onChange={updateAutoCreate}
name="enabled"
name='enabled'
checked={data.autoCreate}
/>
</Grid>
@ -209,24 +209,24 @@ export const GoogleAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Email domains"
name="emailDomains"
label='Email domains'
name='emailDomains'
value={data.emailDomains}
placeholder="@company.com, @anotherCompany.com"
placeholder='@company.com, @anotherCompany.com'
style={{ width: '400px' }}
rows={2}
multiline
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<Button
variant="contained"
color="primary"
type="submit"
variant='contained'
color='primary'
type='submit'
disabled={loading}
>
Save

View File

@ -85,12 +85,12 @@ export const OidcAuth = () => {
<>
<Grid container sx={{ mb: 3 }}>
<Grid item md={12}>
<Alert severity="info">
<Alert severity='info'>
Please read the{' '}
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication"
target="_blank"
rel="noreferrer"
href='https://www.unleash-hosted.com/docs/enterprise-authentication'
target='_blank'
rel='noreferrer'
>
documentation
</a>{' '}
@ -113,7 +113,7 @@ export const OidcAuth = () => {
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
name='enabled'
checked={data.enabled}
/>
}
@ -129,13 +129,13 @@ export const OidcAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Discover URL"
name="discoverUrl"
label='Discover URL'
name='discoverUrl'
value={data.discoverUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
@ -147,13 +147,13 @@ export const OidcAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Client ID"
name="clientId"
label='Client ID'
name='clientId'
value={data.clientId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -168,13 +168,13 @@ export const OidcAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Client Secret"
name="secret"
label='Client Secret'
name='secret'
value={data.secret}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -195,7 +195,7 @@ export const OidcAuth = () => {
onChange={updateSingleSignOut}
value={data.enableSingleSignOut}
disabled={!data.enabled}
name="enableSingleSignOut"
name='enableSingleSignOut'
checked={data.enableSingleSignOut}
/>
}
@ -222,18 +222,18 @@ export const OidcAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="ACR Values"
name="acrValues"
label='ACR Values'
name='acrValues'
value={data.acrValues}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
<SsoGroupSettings
ssoType="OIDC"
ssoType='OIDC'
data={data}
setValue={setValue}
/>
@ -250,26 +250,26 @@ export const OidcAuth = () => {
</Grid>
<Grid item md={6}>
<FormControl style={{ minWidth: '200px' }}>
<InputLabel id="defaultRootRole-label">
<InputLabel id='defaultRootRole-label'>
Signing algorithm
</InputLabel>
<Select
label="Signing algorithm"
labelId="idTokenSigningAlgorithm-label"
id="idTokenSigningAlgorithm"
name="idTokenSigningAlgorithm"
label='Signing algorithm'
labelId='idTokenSigningAlgorithm-label'
id='idTokenSigningAlgorithm'
name='idTokenSigningAlgorithm'
value={data.idTokenSigningAlgorithm || 'RS256'}
onChange={e =>
onChange={(e) =>
setValue(
'idTokenSigningAlgorithm',
e.target.value
e.target.value,
)
}
>
{/*consider these from API or constants. */}
<MenuItem value="RS256">RS256</MenuItem>
<MenuItem value="RS384">RS384</MenuItem>
<MenuItem value="RS512">RS512</MenuItem>
<MenuItem value='RS256'>RS256</MenuItem>
<MenuItem value='RS384'>RS384</MenuItem>
<MenuItem value='RS512'>RS512</MenuItem>
</Select>
</FormControl>
</Grid>
@ -277,9 +277,9 @@ export const OidcAuth = () => {
<Grid container spacing={3}>
<Grid item md={12}>
<Button
variant="contained"
color="primary"
type="submit"
variant='contained'
color='primary'
type='submit'
disabled={loading}
>
Save

View File

@ -64,23 +64,23 @@ export const PasswordAuth = () => {
return (
<>
<form onSubmit={onSubmit}>
<Alert severity="info" sx={{ mb: 3 }}>
<Alert severity='info' sx={{ mb: 3 }}>
Overview of administrators on your Unleash instance:
<br />
<br />
<strong>Password based administrators: </strong>{' '}
<Link to="/admin/users">{adminCount?.password}</Link>
<Link to='/admin/users'>{adminCount?.password}</Link>
<br />
<strong>Other administrators: </strong>{' '}
<Link to="/admin/users">{adminCount?.noPassword}</Link>
<Link to='/admin/users'>{adminCount?.noPassword}</Link>
<br />
<strong>Admin service accounts: </strong>{' '}
<Link to="/admin/service-accounts">
<Link to='/admin/service-accounts'>
{adminCount?.service}
</Link>
<br />
<strong>Admin API tokens: </strong>{' '}
<Link to="/admin/api">
<Link to='/admin/api'>
{tokens.filter(({ type }) => type === 'admin').length}
</Link>
</Alert>
@ -95,7 +95,7 @@ export const PasswordAuth = () => {
<Switch
onChange={updateDisabled}
value={!disablePasswordAuth}
name="disabled"
name='disabled'
checked={!disablePasswordAuth}
/>
}
@ -108,9 +108,9 @@ export const PasswordAuth = () => {
<Grid container spacing={3}>
<Grid item md={12}>
<Button
variant="contained"
color="primary"
type="submit"
variant='contained'
color='primary'
type='submit'
disabled={loading}
>
Save

View File

@ -24,11 +24,11 @@ export const PasswordAuthDialog = ({
setOpen(false);
}}
onClick={onClick}
title="Disable password based login?"
primaryButtonText="Disable password based login"
secondaryButtonText="Cancel"
title='Disable password based login?'
primaryButtonText='Disable password based login'
secondaryButtonText='Cancel'
>
<Alert severity="warning">
<Alert severity='warning'>
<strong>Warning!</strong> Disabling password based login may lock
you out of the system permanently if you do not have any alternative
admin credentials (such as an admin SSO account or admin API token)

View File

@ -76,12 +76,12 @@ export const SamlAuth = () => {
<>
<Grid container sx={{ mb: 3 }}>
<Grid item md={12}>
<Alert severity="info">
<Alert severity='info'>
Please read the{' '}
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication"
target="_blank"
rel="noreferrer"
href='https://www.unleash-hosted.com/docs/enterprise-authentication'
target='_blank'
rel='noreferrer'
>
documentation
</a>{' '}
@ -104,7 +104,7 @@ export const SamlAuth = () => {
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
name='enabled'
checked={data.enabled}
/>
}
@ -120,13 +120,13 @@ export const SamlAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Entity ID"
name="entityId"
label='Entity ID'
name='entityId'
value={data.entityId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -142,13 +142,13 @@ export const SamlAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Single Sign-On URL"
name="signOnUrl"
label='Single Sign-On URL'
name='signOnUrl'
value={data.signOnUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -164,8 +164,8 @@ export const SamlAuth = () => {
<Grid item md={7}>
<TextField
onChange={updateField}
label="X.509 Certificate"
name="certificate"
label='X.509 Certificate'
name='certificate'
value={data.certificate}
disabled={!data.enabled}
style={{ width: '100%' }}
@ -178,8 +178,8 @@ export const SamlAuth = () => {
multiline
rows={14}
maxRows={14}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -196,13 +196,13 @@ export const SamlAuth = () => {
<Grid item md={6}>
<TextField
onChange={updateField}
label="Single Sign-out URL"
name="signOutUrl"
label='Single Sign-out URL'
name='signOutUrl'
value={data.signOutUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
@ -219,8 +219,8 @@ export const SamlAuth = () => {
<Grid item md={7}>
<TextField
onChange={updateField}
label="X.509 Certificate"
name="spCertificate"
label='X.509 Certificate'
name='spCertificate'
value={data.spCertificate}
disabled={!data.enabled}
style={{ width: '100%' }}
@ -233,14 +233,14 @@ export const SamlAuth = () => {
multiline
rows={14}
maxRows={14}
variant="outlined"
size="small"
variant='outlined'
size='small'
/>
</Grid>
</Grid>
<SsoGroupSettings
ssoType="SAML"
ssoType='SAML'
data={data}
setValue={setValue}
/>
@ -249,9 +249,9 @@ export const SamlAuth = () => {
<Grid container spacing={3}>
<Grid item md={5}>
<Button
variant="contained"
color="primary"
type="submit"
variant='contained'
color='primary'
type='submit'
disabled={loading}
>
Save

View File

@ -51,7 +51,7 @@ export const SsoGroupSettings = ({
<Switch
onChange={updateGroupSyncing}
value={data.enableGroupSyncing}
name="enableGroupSyncing"
name='enableGroupSyncing'
checked={data.enableGroupSyncing}
disabled={!data.enabled}
/>
@ -71,13 +71,13 @@ export const SsoGroupSettings = ({
<Grid item md={6}>
<TextField
onChange={updateField}
label="Group JSON Path"
name="groupJsonPath"
label='Group JSON Path'
name='groupJsonPath'
value={data.groupJsonPath}
disabled={!data.enableGroupSyncing}
style={{ width: '400px' }}
variant="outlined"
size="small"
variant='outlined'
size='small'
required
/>
</Grid>
@ -100,7 +100,7 @@ export const SsoGroupSettings = ({
onChange={updateAddGroupScope}
value={data.addGroupsScope}
disabled={!data.enableGroupSyncing}
name="addGroupsScope"
name='addGroupsScope'
checked={data.addGroupsScope}
/>
}

View File

@ -29,7 +29,7 @@ export const Billing = () => {
return (
<div>
<PageContent header="Billing" isLoading={loading}>
<PageContent header='Billing' isLoading={loading}>
<ConditionallyRender
condition={isBilling}
show={
@ -43,7 +43,7 @@ export const Billing = () => {
</PermissionGuard>
}
elseShow={
<Alert severity="error">
<Alert severity='error'>
Billing is not enabled for this instance.
</Alert>
}

View File

@ -40,11 +40,11 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
return (
<Grid item xs={12} md={5}>
<StyledInfoBox>
<StyledTitle variant="body1">Billing information</StyledTitle>
<StyledTitle variant='body1'>Billing information</StyledTitle>
<ConditionallyRender
condition={inactive}
show={
<StyledAlert severity="warning">
<StyledAlert severity='warning'>
In order to <strong>Upgrade trial</strong> you need
to provide us your billing information.
</StyledAlert>
@ -58,7 +58,7 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
</StyledInfoLabel>
<StyledDivider />
<StyledInfoLabel>
<a href="mailto:elise@getunleash.ai?subject=PRO plan clarifications">
<a href='mailto:elise@getunleash.ai?subject=PRO plan clarifications'>
Get in touch with us
</a>{' '}
for any clarification

View File

@ -101,20 +101,22 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
<ConditionallyRender
condition={inactive}
show={
<StyledAlert severity="info">
<StyledAlert severity='info'>
After you have sent your billing information, your
instance will be upgraded - you don't have to do
anything.{' '}
<a href="mailto:elise@getunleash.ai?subject=PRO plan clarifications">
<a href='mailto:elise@getunleash.ai?subject=PRO plan clarifications'>
Get in touch with us
</a>{' '}
for any clarification
</StyledAlert>
}
/>
<Badge color="success">Current plan</Badge>
<Badge color='success'>Current plan</Badge>
<Grid container>
<GridRow sx={theme => ({ marginBottom: theme.spacing(3) })}>
<GridRow
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
>
<GridCol>
<StyledPlanSpan>
{instanceStatus.plan}
@ -123,7 +125,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
condition={isTrialInstance(instanceStatus)}
show={
<StyledTrialSpan
sx={theme => ({
sx={(theme) => ({
color: expired
? theme.palette.error.dark
: theme.palette.warning.dark,
@ -153,13 +155,13 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.proPlanAutoCharge &&
instanceStatus.plan === InstancePlan.PRO
instanceStatus.plan === InstancePlan.PRO,
)}
show={
<>
<Grid container>
<GridRow
sx={theme => ({
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
>
@ -167,7 +169,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
<Typography>
<strong>Included members</strong>
<GridColLink>
<Link to="/admin/users">
<Link to='/admin/users'>
{freeAssigned} of 5 assigned
</Link>
</GridColLink>
@ -179,7 +181,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
</GridCol>
<GridCol>
<StyledCheckIcon />
<Typography variant="body2">
<Typography variant='body2'>
included
</Typography>
</GridCol>
@ -189,7 +191,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
<Typography>
<strong>Paid members</strong>
<GridColLink>
<Link to="/admin/users">
<Link to='/admin/users'>
{paidAssigned} assigned
</Link>
</GridColLink>
@ -200,7 +202,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
</GridCol>
<GridCol>
<Typography
sx={theme => ({
sx={(theme) => ({
fontSize:
theme.fontSizes.mainHeader,
})}
@ -215,7 +217,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
<GridRow>
<GridCol>
<Typography
sx={theme => ({
sx={(theme) => ({
fontWeight:
theme.fontWeight.bold,
fontSize:
@ -227,7 +229,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
</GridCol>
<GridCol>
<Typography
sx={theme => ({
sx={(theme) => ({
fontWeight:
theme.fontWeight.bold,
fontSize: '2rem',

View File

@ -78,7 +78,7 @@ export const BillingHistory: VFC<IBillingHistoryProps> = ({
() => ({
sortBy: [{ id: 'created' }],
}),
[]
[],
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
@ -95,7 +95,7 @@ export const BillingHistory: VFC<IBillingHistoryProps> = ({
},
},
useGlobalFilter,
useSortBy
useSortBy,
);
return (
@ -104,11 +104,11 @@ export const BillingHistory: VFC<IBillingHistoryProps> = ({
<Table {...getTableProps()}>
<SortableTableHeader headerGroups={headerGroups} />
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
{rows.map((row) => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>

View File

@ -13,7 +13,7 @@ const FlaggedBillingRedirect = () => {
return <InvoiceAdminPage />;
}
return <Navigate to="/admin/billing" replace />;
return <Navigate to='/admin/billing' replace />;
};
export default FlaggedBillingRedirect;

View File

@ -57,10 +57,10 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
aria-describedby={helpTextId}
placeholder={textareaDomainsPlaceholder}
value={value}
onChange={event => setValue(event.target.value)}
onChange={(event) => setValue(event.target.value)}
multiline
rows={12}
variant="outlined"
variant='outlined'
fullWidth
InputProps={{
style: { fontFamily: 'monospace', fontSize: '0.8em' },
@ -75,7 +75,7 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
export const parseInputValue = (value: string): string[] => {
return value
.split(/[,\n\s]+/) // Split by commas/newlines/spaces.
.map(value => value.replace(/\/$/, '')) // Remove trailing slashes.
.map((value) => value.replace(/\/$/, '')) // Remove trailing slashes.
.filter(Boolean); // Remove empty values from (e.g.) double newlines.
};

View File

@ -3,7 +3,7 @@ import { Alert } from '@mui/material';
export const CorsHelpAlert = () => {
return (
<Alert severity="info">
<Alert severity='info'>
<p>
Use this page to configure allowed CORS origins for the Frontend
API (<code>/api/frontend</code>).

View File

@ -23,7 +23,7 @@ const CorsPage = () => {
}
return (
<PageContent header={<PageHeader title="CORS origins" />}>
<PageContent header={<PageHeader title='CORS origins' />}>
<Box sx={{ display: 'grid', gap: 4 }}>
<CorsHelpAlert />
<CorsForm frontendApiOrigins={uiConfig.frontendApiOrigins} />

View File

@ -9,8 +9,8 @@ describe('filterAdminRoutes - open souce routes', () => {
pro: false,
enterprise: false,
billing: false,
}
)
},
),
).toBe(true);
});
@ -24,7 +24,7 @@ describe('filterAdminRoutes - open souce routes', () => {
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(false);
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(false);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
false
false,
);
expect(filterAdminRoutes({ billing: true }, state)).toBe(false);
});
@ -38,7 +38,7 @@ describe('filterAdminRoutes - open souce routes', () => {
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(true);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
true
true,
);
// This is to show enterprise badge in pro mode
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(true);
@ -53,7 +53,7 @@ describe('filterAdminRoutes - open souce routes', () => {
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(true);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
true
true,
);
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(false);
});
@ -66,8 +66,8 @@ describe('filterAdminRoutes - open souce routes', () => {
pro: true,
enterprise: false,
billing: true,
}
)
},
),
).toBe(true);
expect(
filterAdminRoutes(
@ -76,8 +76,8 @@ describe('filterAdminRoutes - open souce routes', () => {
pro: false,
enterprise: true,
billing: true,
}
)
},
),
).toBe(true);
expect(
filterAdminRoutes(
@ -86,8 +86,8 @@ describe('filterAdminRoutes - open souce routes', () => {
pro: true,
enterprise: false,
billing: true,
}
)
},
),
).toBe(true);
expect(
filterAdminRoutes(
@ -96,8 +96,8 @@ describe('filterAdminRoutes - open souce routes', () => {
pro: false,
enterprise: false,
billing: true,
}
)
},
),
).toBe(false);
});
});

View File

@ -6,7 +6,7 @@ export const filterAdminRoutes = (
pro,
enterprise,
billing,
}: { pro?: boolean; enterprise?: boolean; billing?: boolean }
}: { pro?: boolean; enterprise?: boolean; billing?: boolean },
): boolean => {
const mode = menu?.mode;
if (menu?.billing && !billing) return false;

View File

@ -59,9 +59,7 @@ export const CreateGroup = () => {
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/groups' \\
return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/groups' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getGroupPayload(), undefined, 2)}'`;
@ -73,7 +71,7 @@ export const CreateGroup = () => {
const isNameNotEmpty = (name: string) => name.length;
const isNameUnique = (name: string) =>
!groups?.filter(group => group.name === name).length;
!groups?.filter((group) => group.name === name).length;
const isValid = isNameNotEmpty(name) && isNameUnique(name);
const onSetName = (name: string) => {
@ -87,10 +85,10 @@ export const CreateGroup = () => {
return (
<FormTemplate
loading={loading}
title="Create group"
description="Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group."
documentationLink="https://docs.getunleash.io/advanced/groups"
documentationLinkLabel="Groups documentation"
title='Create group'
description='Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group.'
documentationLink='https://docs.getunleash.io/advanced/groups'
documentationLinkLabel='Groups documentation'
formatApiCode={formatApiCode}
>
<GroupForm
@ -110,9 +108,9 @@ export const CreateGroup = () => {
mode={CREATE}
>
<Button
type="submit"
variant="contained"
color="primary"
type='submit'
variant='contained'
color='primary'
disabled={!isValid}
data-testid={UG_CREATE_BTN_ID}
>

View File

@ -66,7 +66,7 @@ export const EditGroup = ({
group?.description,
group?.mappingsSSO,
group?.users,
group?.rootRole
group?.rootRole,
);
const { groups } = useGroups();
@ -106,7 +106,7 @@ export const EditGroup = ({
const isNameNotEmpty = (name: string) => name.length;
const isNameUnique = (name: string) =>
!groups?.filter(group => group.name === name && group.id !== groupId)
!groups?.filter((group) => group.name === name && group.id !== groupId)
.length;
const isValid = isNameNotEmpty(name) && isNameUnique(name);
@ -121,10 +121,10 @@ export const EditGroup = ({
return (
<FormTemplate
loading={loading}
title="Edit group"
description="Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group."
documentationLink="https://docs.getunleash.io/advanced/groups"
documentationLinkLabel="Groups documentation"
title='Edit group'
description='Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group.'
documentationLink='https://docs.getunleash.io/advanced/groups'
documentationLinkLabel='Groups documentation'
formatApiCode={formatApiCode}
>
<GroupForm
@ -144,9 +144,9 @@ export const EditGroup = ({
mode={EDIT}
>
<Button
type="submit"
variant="contained"
color="primary"
type='submit'
variant='contained'
color='primary'
disabled={!isValid}
data-testid={UG_SAVE_BTN_ID}
>

View File

@ -61,7 +61,7 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
group.description,
group.mappingsSSO,
group.users,
group.rootRole
group.rootRole,
);
useEffect(() => {
@ -100,15 +100,15 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
onClose={() => {
setOpen(false);
}}
label="Edit users"
label='Edit users'
>
<FormTemplate
loading={loading}
modal
title="Edit users"
description="Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group."
documentationLink="https://docs.getunleash.io/advanced/groups"
documentationLinkLabel="Groups documentation"
title='Edit users'
description='Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group.'
documentationLink='https://docs.getunleash.io/advanced/groups'
documentationLinkLabel='Groups documentation'
formatApiCode={formatApiCode}
>
<StyledForm onSubmit={handleSubmit}>
@ -129,9 +129,9 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
<StyledButtonContainer>
<StyledBox>
<Button
type="submit"
variant="contained"
color="primary"
type='submit'
variant='contained'
color='primary'
data-testid={UG_SAVE_BTN_ID}
>
Save

View File

@ -48,7 +48,7 @@ const defaultSort: SortingRule<string> = { id: 'joinedAt' };
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
'Group:v1',
defaultSort
defaultSort,
);
export const Group: VFC = () => {
@ -108,8 +108,8 @@ export const Group: VFC = () => {
Cell: ({ row: { original: user } }: any) => (
<TimeAgoCell
value={user.seenAt}
emptyText="Never"
title={date => `Last login: ${date}`}
emptyText='Never'
title={(date) => `Last login: ${date}`}
/>
),
sortType: 'date',
@ -122,7 +122,7 @@ export const Group: VFC = () => {
Cell: ({ row: { original: rowUser } }: any) => (
<ActionCell>
<Tooltip
title="Remove user from group"
title='Remove user from group'
arrow
describeChild
>
@ -156,7 +156,7 @@ export const Group: VFC = () => {
searchable: true,
},
],
[setSelectedUser, setRemoveUserOpen]
[setSelectedUser, setRemoveUserOpen],
);
const [searchParams, setSearchParams] = useSearchParams();
@ -185,7 +185,7 @@ export const Group: VFC = () => {
searchedData?.length === 0 && loading
? groupUsersPlaceholder
: searchedData,
[searchedData, loading]
[searchedData, loading],
);
const {
@ -204,7 +204,7 @@ export const Group: VFC = () => {
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
useEffect(() => {
@ -296,7 +296,7 @@ export const Group: VFC = () => {
onClick={() => {
setEditUsersOpen(true);
}}
maxWidth="700px"
maxWidth='700px'
Icon={Add}
permission={ADMIN}
>

View File

@ -50,13 +50,13 @@ export const RemoveGroupUser: FC<IRemoveGroupUserProps> = ({
return (
<Dialogue
open={open && Boolean(user)}
primaryButtonText="Remove user"
secondaryButtonText="Cancel"
primaryButtonText='Remove user'
secondaryButtonText='Cancel'
onClick={onRemoveClick}
onClose={() => {
setOpen(false);
}}
title="Remove user from group?"
title='Remove user from group?'
>
<Typography>
Do you really want to remove <strong>{userName}</strong> from{' '}

View File

@ -128,12 +128,12 @@ export const GroupForm: FC<IGroupForm> = ({
</StyledInputDescription>
<StyledInput
autoFocus
label="Name"
id="group-name"
label='Name'
id='group-name'
error={Boolean(errors.name)}
errorText={errors.name}
value={name}
onChange={e => setName(e.target.value)}
onChange={(e) => setName(e.target.value)}
data-testid={UG_NAME_ID}
required
/>
@ -143,10 +143,10 @@ export const GroupForm: FC<IGroupForm> = ({
<StyledInput
multiline
rows={4}
label="Description"
placeholder="A short description of the group"
label='Description'
placeholder='A short description of the group'
value={description}
onChange={e => setDescription(e.target.value)}
onChange={(e) => setDescription(e.target.value)}
data-testid={UG_DESC_ID}
/>
<ConditionallyRender
@ -157,7 +157,7 @@ export const GroupForm: FC<IGroupForm> = ({
Is this group associated with SSO groups?
</StyledInputDescription>
<StyledItemList
label="SSO group ID / name"
label='SSO group ID / name'
value={mappingsSSO}
onChange={setMappingsSSO}
/>
@ -168,7 +168,7 @@ export const GroupForm: FC<IGroupForm> = ({
<Box sx={{ display: 'flex' }}>
You can enable SSO groups synchronization if
needed
<HelpIcon tooltip="SSO groups synchronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." />
<HelpIcon tooltip='SSO groups synchronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized.' />
</Box>
<Link data-loading to={`/admin/auth`}>
<span data-loading>View SSO configuration</span>
@ -179,15 +179,15 @@ export const GroupForm: FC<IGroupForm> = ({
<StyledInputDescription>
<Box sx={{ display: 'flex' }}>
Do you want to associate a root role with this group?
<HelpIcon tooltip="When you associate a root role with this group, users in this group will automatically inherit the role globally." />
<HelpIcon tooltip='When you associate a root role with this group, users in this group will automatically inherit the role globally.' />
</Box>
</StyledInputDescription>
<StyledAutocompleteWrapper>
<RoleSelect
data-testid="GROUP_ROOT_ROLE"
data-testid='GROUP_ROOT_ROLE'
roles={roles}
value={roleIdToRole(rootRole)}
setValue={role => setRootRole(role?.id || null)}
setValue={(role) => setRootRole(role?.id || null)}
/>
</StyledAutocompleteWrapper>
<ConditionallyRender

View File

@ -35,12 +35,12 @@ const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({
const renderOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: IUser,
selected: boolean
selected: boolean,
) => (
<li {...props}>
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
checkedIcon={<CheckBoxIcon fontSize='small' />}
style={{ marginRight: 8 }}
checked={selected}
/>
@ -103,7 +103,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
<StyledGroupFormUsersSelect>
<Autocomplete
data-testid={UG_USERS_ID}
size="small"
size='small'
multiple
limitTags={1}
openOnFocus
@ -119,7 +119,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
}
setUsers(newValue);
}}
groupBy={option => option.type}
groupBy={(option) => option.type}
options={options}
renderOption={(props, option, { selected }) =>
renderOption(props, option as UserOption, selected)
@ -129,17 +129,17 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
({ name, username, email }) =>
caseInsensitiveSearch(inputValue, email) ||
caseInsensitiveSearch(inputValue, name) ||
caseInsensitiveSearch(inputValue, username)
caseInsensitiveSearch(inputValue, username),
)
}
isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option: UserOption) =>
option.email || option.name || option.username || ''
}
renderInput={params => (
<TextField {...params} label="Select users" />
renderInput={(params) => (
<TextField {...params} label='Select users' />
)}
renderTags={value => renderTags(value)}
renderTags={(value) => renderTags(value)}
/>
</StyledGroupFormUsersSelect>
);

View File

@ -59,7 +59,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
Cell: ({ row: { original: rowUser } }: any) => (
<ActionCell>
<Tooltip
title="Remove user from group"
title='Remove user from group'
arrow
describeChild
>
@ -67,8 +67,8 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
onClick={() =>
setUsers((users: IGroupUser[]) =>
users.filter(
user => user.id !== rowUser.id
)
(user) => user.id !== rowUser.id,
),
)
}
>
@ -93,7 +93,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
searchable: true,
},
],
[setUsers]
[setUsers],
);
const [initialState] = useState(() => ({
@ -112,7 +112,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
useConditionallyHiddenColumns(
@ -123,7 +123,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
},
],
setHiddenColumns,
columns
columns,
);
return (

View File

@ -18,17 +18,17 @@ export const GroupsAdmin = () => {
<PermissionGuard permissions={ADMIN}>
<Routes>
<Route index element={<GroupsList />} />
<Route path="create-group" element={<CreateGroup />} />
<Route path='create-group' element={<CreateGroup />} />
<Route
path=":groupId/edit"
path=':groupId/edit'
element={<EditGroupContainer />}
/>
<Route path=":groupId" element={<Group />} />
<Route path=':groupId' element={<Group />} />
</Routes>
</PermissionGuard>
</div>
);
}
return <PremiumFeature feature="groups" page />;
return <PremiumFeature feature='groups' page />;
};

View File

@ -134,22 +134,22 @@ export const GroupCard = ({
<ProjectBadgeContainer>
<ConditionallyRender
condition={group.projects.length > 0}
show={group.projects.map(project => (
show={group.projects.map((project) => (
<Tooltip
key={project}
title="View project"
title='View project'
arrow
placement="bottom-end"
placement='bottom-end'
describeChild
>
<Badge
onClick={e => {
onClick={(e) => {
e.preventDefault();
navigate(
`/projects/${project}/settings/access`
`/projects/${project}/settings/access`,
);
}}
color="secondary"
color='secondary'
icon={<TopicOutlinedIcon />}
>
{project}
@ -158,7 +158,7 @@ export const GroupCard = ({
))}
elseShow={
<Tooltip
title="This group is not used in any project"
title='This group is not used in any project'
arrow
describeChild
>

View File

@ -50,19 +50,19 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
return (
<StyledActions
onClick={e => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<Tooltip title="Group actions" arrow describeChild>
<Tooltip title='Group actions' arrow describeChild>
<IconButton
id={id}
aria-controls={open ? menuId : undefined}
aria-haspopup="true"
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
type="button"
type='button'
>
<MoreVert />
</IconButton>
@ -86,7 +86,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
<Edit />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">Edit group</Typography>
<Typography variant='body2'>Edit group</Typography>
</ListItemText>
</MenuItem>
<MenuItem
@ -99,7 +99,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
<GroupRounded />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
<Typography variant='body2'>
Edit group users
</Typography>
</ListItemText>
@ -114,7 +114,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
<Delete />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
<Typography variant='body2'>
Delete group
</Typography>
</ListItemText>

View File

@ -30,7 +30,7 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
users
.sort((a, b) => b?.joinedAt!.getTime() - a?.joinedAt!.getTime())
.slice(0, 9),
[users]
[users],
);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
@ -48,11 +48,11 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
return (
<StyledAvatars>
{shownUsers.map(user => (
{shownUsers.map((user) => (
<StyledAvatar
key={user.id}
user={user}
onMouseEnter={event => {
onMouseEnter={(event) => {
onPopoverOpen(event);
setPopupUser(user);
}}

View File

@ -23,10 +23,10 @@ export const GroupEmpty = () => {
No groups available. Get started by adding a new group.
</StyledTitle>
<Button
to="/admin/groups/create-group"
to='/admin/groups/create-group'
component={Link}
variant="outlined"
color="primary"
variant='outlined'
color='primary'
>
Create your first group
</Button>

View File

@ -24,16 +24,18 @@ type PageQueryType = Partial<Record<'search', string>>;
const groupsSearch = (group: IGroup, searchValue: string) => {
const search = searchValue.toLowerCase();
const users = {
names: group.users?.map(user => user.name?.toLowerCase() || ''),
usernames: group.users?.map(user => user.username?.toLowerCase() || ''),
emails: group.users?.map(user => user.email?.toLowerCase() || ''),
names: group.users?.map((user) => user.name?.toLowerCase() || ''),
usernames: group.users?.map(
(user) => user.username?.toLowerCase() || '',
),
emails: group.users?.map((user) => user.email?.toLowerCase() || ''),
};
return (
group.name.toLowerCase().includes(search) ||
group.description?.toLowerCase().includes(search) ||
users.names?.some(name => name.includes(search)) ||
users.usernames?.some(username => username.includes(search)) ||
users.emails?.some(email => email.includes(search))
users.names?.some((name) => name.includes(search)) ||
users.usernames?.some((username) => username.includes(search)) ||
users.emails?.some((email) => email.includes(search))
);
};
@ -42,12 +44,12 @@ export const GroupsList: VFC = () => {
const [editUsersOpen, setEditUsersOpen] = useState(false);
const [removeOpen, setRemoveOpen] = useState(false);
const [activeGroup, setActiveGroup] = useState<IGroup | undefined>(
undefined
undefined,
);
const { groups = [], loading } = useGroups();
const [searchParams, setSearchParams] = useSearchParams();
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
searchParams.get('search') || '',
);
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
@ -65,10 +67,10 @@ export const GroupsList: VFC = () => {
const data = useMemo(() => {
const sortedGroups = groups.sort((a, b) =>
a.name.localeCompare(b.name)
a.name.localeCompare(b.name),
);
return searchValue
? sortedGroups.filter(group => groupsSearch(group, searchValue))
? sortedGroups.filter((group) => groupsSearch(group, searchValue))
: sortedGroups;
}, [groups, searchValue]);
@ -106,7 +108,7 @@ export const GroupsList: VFC = () => {
onClick={() =>
navigate('/admin/groups/create-group')
}
maxWidth="700px"
maxWidth='700px'
Icon={Add}
permission={ADMIN}
data-testid={NAVIGATE_TO_CREATE_GROUP}
@ -130,7 +132,7 @@ export const GroupsList: VFC = () => {
>
<SearchHighlightProvider value={searchValue}>
<Grid container spacing={2}>
{data.map(group => (
{data.map((group) => (
<Grid key={group.id} item xs={12} md={6}>
<GroupCard
group={group}

View File

@ -42,13 +42,13 @@ export const RemoveGroup: FC<IRemoveGroupProps> = ({
return (
<Dialogue
open={open}
primaryButtonText="Delete group"
secondaryButtonText="Cancel"
primaryButtonText='Delete group'
secondaryButtonText='Cancel'
onClick={onRemoveClick}
onClose={() => {
setOpen(false);
}}
title="Delete group?"
title='Delete group?'
>
<Typography>
Do you really want to delete <strong>{group.name}</strong>?

View File

@ -7,7 +7,7 @@ export const useGroupForm = (
initialDescription = '',
initialMappingsSSO: string[] = [],
initialUsers: IGroupUser[] = [],
initialRootRole: number | null = null
initialRootRole: number | null = null,
) => {
const params = useQueryParams();
const groupQueryName = params.get('name');

View File

@ -46,27 +46,27 @@ export const InstanceStats: VFC = () => {
if (stats?.versionEnterprise) {
rows.push(
{ title: 'SAML enabled', value: stats?.SAMLenabled ? 'Yes' : 'No' },
{ title: 'OIDC enabled', value: stats?.OIDCenabled ? 'Yes' : 'No' }
{ title: 'OIDC enabled', value: stats?.OIDCenabled ? 'Yes' : 'No' },
);
}
return (
<PageContent header={<PageHeader title="Instance Statistics" />}>
<PageContent header={<PageHeader title='Instance Statistics' />}>
<Box sx={{ display: 'grid', gap: 4 }}>
<Table aria-label="Instance statistics">
<Table aria-label='Instance statistics'>
<TableHead>
<TableRow>
<TableCell>Field</TableCell>
<TableCell align="right">Value</TableCell>
<TableCell align='right'>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
{rows.map((row) => (
<TableRow key={row.title}>
<TableCell component="th" scope="row">
<TableCell component='th' scope='row'>
<Box
component="span"
sx={theme => ({
component='span'
sx={(theme) => ({
marginLeft: row.offset
? theme.spacing(2)
: 0,
@ -75,7 +75,7 @@ export const InstanceStats: VFC = () => {
{row.title}
</Box>
</TableCell>
<TableCell align="right">{row.value}</TableCell>
<TableCell align='right'>{row.value}</TableCell>
</TableRow>
))}
</TableBody>
@ -83,13 +83,13 @@ export const InstanceStats: VFC = () => {
<span style={{ textAlign: 'center' }}>
<Button
startIcon={<Download />}
aria-label="Download instance statistics"
color="primary"
variant="contained"
target="_blank"
rel="noreferrer"
aria-label='Download instance statistics'
color='primary'
variant='contained'
target='_blank'
rel='noreferrer'
href={formatApiPath(
'/api/admin/instance-admin/statistics/csv'
'/api/admin/instance-admin/statistics/csv',
)}
>
Download

View File

@ -109,7 +109,7 @@ export const InstancePrivacy = () => {
: 'When you enable feature usage collection you must also enable version info collection';
return (
<PageContent header={<PageHeader title="Instance Privacy" />}>
<PageContent header={<PageHeader title='Instance Privacy' />}>
<StyledBox>
<InstancePrivacySection
title={versionCollectionDetails.title}

View File

@ -153,12 +153,12 @@ export const InstancePrivacySection = ({
<ConditionallyRender
condition={enabled}
show={
<Badge color="success" icon={<CheckIcon />}>
<Badge color='success' icon={<CheckIcon />}>
Data is collected
</Badge>
}
elseShow={
<Badge color="neutral" icon={<ClearIcon />}>
<Badge color='neutral' icon={<ClearIcon />}>
No data is collected
</Badge>
}

View File

@ -37,12 +37,12 @@ const InvoiceList = () => {
<PageContent
header={
<PageHeader
title="Invoices"
title='Invoices'
actions={
<Button
href={PORTAL_URL}
rel="noreferrer"
target="_blank"
rel='noreferrer'
target='_blank'
endIcon={<OpenInNew />}
>
Billing portal
@ -89,7 +89,7 @@ const InvoiceList = () => {
{item.dueDate &&
formatDateYMD(
item.dueDate,
locationSettings.locale
locationSettings.locale,
)}
</TableCell>
<TableCell
@ -102,8 +102,8 @@ const InvoiceList = () => {
>
<a
href={item.invoiceURL}
target="_blank"
rel="noreferrer"
target='_blank'
rel='noreferrer'
>
Payment link
</a>

View File

@ -67,7 +67,7 @@ export const MaintenanceToggle = () => {
<Switch
onChange={updateEnabled}
value={enabled}
name="enabled"
name='enabled'
checked={enabled}
/>
}

View File

@ -3,7 +3,7 @@ import { Alert } from '@mui/material';
export const MaintenanceTooltip = () => {
return (
<Alert severity="warning">
<Alert severity='warning'>
<p>
<b>Heads up!</b> If you enable maintenance mode, edit access in
the entire system will be disabled for all the users (admins,

View File

@ -28,7 +28,7 @@ const MaintenancePage = () => {
}
return (
<PageContent header={<PageHeader title="Maintenance" />}>
<PageContent header={<PageHeader title='Maintenance' />}>
<StyledBox>
<MaintenanceTooltip />
<MaintenanceToggle />

View File

@ -27,15 +27,15 @@ export const AdminTabsMenu: VFC = () => {
const activeTab = pathname.split('/')[2];
const adminRoutes = useAdminRoutes();
const group = adminRoutes.find(route =>
pathname.includes(route.path)
const group = adminRoutes.find((route) =>
pathname.includes(route.path),
)?.group;
const tabs = adminRoutes.filter(
route =>
(route) =>
!group ||
route.group === group ||
(isOss() && route.group !== 'log')
(isOss() && route.group !== 'log'),
);
if (!group) {
@ -46,11 +46,11 @@ export const AdminTabsMenu: VFC = () => {
<StyledPaper>
<Tabs
value={activeTab}
variant="scrollable"
scrollButtons="auto"
variant='scrollable'
scrollButtons='auto'
allowScrollButtonsMobile
>
{tabs.map(tab => (
{tabs.map((tab) => (
<Tab
sx={{ padding: 0 }}
key={tab.route}
@ -62,7 +62,7 @@ export const AdminTabsMenu: VFC = () => {
condition={Boolean(
tab.menu.mode?.includes('enterprise') &&
!tab.menu.mode?.includes('pro') &&
isPro()
isPro(),
)}
show={
<StyledBadgeContainer>

View File

@ -29,9 +29,9 @@ export const Network = () => {
header={
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
indicatorColor='primary'
textColor='primary'
variant='scrollable'
allowScrollButtonsMobile
>
{tabs.map(({ label, path }) => (
@ -50,8 +50,8 @@ export const Network = () => {
}
>
<Routes>
<Route path="traffic" element={<NetworkTraffic />} />
<Route path="*" element={<NetworkOverview />} />
<Route path='traffic' element={<NetworkTraffic />} />
<Route path='*' element={<NetworkOverview />} />
</Routes>
</PageContent>
</div>

View File

@ -38,10 +38,10 @@ interface INetworkApp {
}
const asNetworkAppData = (
result: RequestsPerSecondSchemaDataResultItem & { label: string }
result: RequestsPerSecondSchemaDataResultItem & { label: string },
) => {
const values = (result.values || []) as ResultValue[];
const data = values.filter(value => isRecent(value));
const data = values.filter((value) => isRecent(value));
const reqs = data.length ? parseFloat(data[data.length - 1][1]) : 0;
return {
label: result.label,
@ -54,7 +54,7 @@ const summingReqsByLabelAndType = (
acc: {
[group: string]: INetworkApp;
},
current: INetworkApp
current: INetworkApp,
) => {
const groupBy = current.label + current.type;
acc[groupBy] = {
@ -67,18 +67,18 @@ const summingReqsByLabelAndType = (
const toGraphData = (metrics?: RequestsPerSecondSchema) => {
const results =
metrics?.data?.result
?.map(result => ({
?.map((result) => ({
...result,
label: unknownify(result.metric?.appName),
}))
.filter(result => result.label !== 'unknown') || [];
.filter((result) => result.label !== 'unknown') || [];
const aggregated = results
.map(asNetworkAppData)
.reduce(summingReqsByLabelAndType, {});
return (
Object.values(aggregated)
.map(app => ({ ...app, reqs: app.reqs.toFixed(2) }))
.filter(app => app.reqs !== '0.00') ?? []
.map((app) => ({ ...app, reqs: app.reqs.toFixed(2) }))
.filter((app) => app.reqs !== '0.00') ?? []
);
};
@ -95,15 +95,15 @@ export const NetworkOverview = () => {
subgraph _[ ]
direction BT
Unleash(<img src='${formatAssetPath(
themeMode === 'dark' ? logoWhiteIcon : logoIcon
themeMode === 'dark' ? logoWhiteIcon : logoIcon,
)}' width='72' height='72' class='unleash-logo'/><br/>Unleash)
${apps
.map(
({ label, reqs, type }, i) =>
`app-${i}("${label.replaceAll(
'"',
'&quot;'
)}") -- ${reqs} req/s<br>${type} --> Unleash`
'&quot;',
)}") -- ${reqs} req/s<br>${type} --> Unleash`,
)
.join('\n')}
end
@ -112,7 +112,7 @@ export const NetworkOverview = () => {
return (
<ConditionallyRender
condition={apps.length === 0}
show={<Alert severity="warning">No data available.</Alert>}
show={<Alert severity='warning'>No data available.</Alert>}
elseShow={<StyledMermaid>{graph}</StyledMermaid>}
/>
);

View File

@ -42,9 +42,9 @@ type ResultValue = [number, string];
const createChartPoints = (
values: ResultValue[],
y: (m: string) => number
y: (m: string) => number,
): IPoint[] => {
return values.map(row => ({
return values.map((row) => ({
x: row[0],
y: y(row[1]),
}));
@ -52,7 +52,7 @@ const createChartPoints = (
const createInstanceChartOptions = (
theme: Theme,
locationSettings: ILocationSettings
locationSettings: ILocationSettings,
): ChartOptions<'line'> => ({
locale: locationSettings.locale,
responsive: true,
@ -73,10 +73,10 @@ const createInstanceChartOptions = (
boxPadding: 5,
usePointStyle: true,
callbacks: {
title: items =>
title: (items) =>
formatDateHM(
1000 * items[0].parsed.x,
locationSettings.locale
locationSettings.locale,
),
},
itemSort: (a, b) => b.parsed.y - a.parsed.y,
@ -155,7 +155,7 @@ class ItemPicker<T> {
const toChartData = (
theme: Theme,
rps?: RequestsPerSecondSchema
rps?: RequestsPerSecondSchema,
): ChartDatasetType[] => {
if (rps?.data?.result) {
const colorPicker = new ItemPicker([
@ -173,7 +173,7 @@ const toChartData = (
label: `${endpoint}: ${appName}`,
borderColor: color.main,
backgroundColor: color.main,
data: createChartPoints(values, y => parseFloat(y)),
data: createChartPoints(values, (y) => parseFloat(y)),
elements: {
point: {
radius: 4,
@ -206,14 +206,14 @@ export const NetworkTraffic: VFC = () => {
return (
<ConditionallyRender
condition={data.datasets.length === 0}
show={<Alert severity="warning">No data available.</Alert>}
show={<Alert severity='warning'>No data available.</Alert>}
elseShow={
<Box sx={{ display: 'grid', gap: 4 }}>
<div style={{ height: 400 }}>
<Line
data={data}
options={options}
aria-label="An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API"
aria-label='An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API'
/>
</div>
</Box>
@ -231,7 +231,7 @@ ChartJS.register(
TimeScale,
Legend,
Tooltip,
Title
Title,
);
// Use a default export to lazy-load the charting library.

View File

@ -60,20 +60,20 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
acc[getRoleKey(curr)] = true;
return acc;
},
{}
{},
) || {},
[permissions]
[permissions],
);
const permissionCount = useMemo(
() =>
Object.keys(checkedPermissions).filter(key => permissionMap[key])
Object.keys(checkedPermissions).filter((key) => permissionMap[key])
.length || 0,
[checkedPermissions, permissionMap]
[checkedPermissions, permissionMap],
);
const isAllChecked = useMemo(
() => permissionCount === permissions?.length,
[permissionCount, permissions]
[permissionCount, permissions],
);
return (
@ -90,14 +90,15 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
boxShadow: 'none',
px: 3,
py: 1,
border: theme => `1px solid ${theme.palette.divider}`,
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: (theme) =>
`${theme.shape.borderRadiusLarge}px`,
}}
>
<AccordionSummary
expandIcon={
<IconButton>
<ExpandMore titleAccess="Toggle" />
<ExpandMore titleAccess='Toggle' />
</IconButton>
}
sx={{
@ -109,10 +110,10 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
{Icon}
<StyledTitle
text={title}
maxWidth="120"
maxWidth='120'
maxLength={25}
/>{' '}
<Typography variant="body2" color="text.secondary">
<Typography variant='body2' color='text.secondary'>
({permissionCount} / {permissions?.length}{' '}
permissions)
</Typography>
@ -127,11 +128,11 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
>
<Divider sx={{ mb: 1 }} />
<Button
variant="text"
size="small"
variant='text'
size='small'
onClick={onCheckAll}
sx={{
fontWeight: theme =>
fontWeight: (theme) =>
theme.typography.fontWeightRegular,
}}
>
@ -154,12 +155,12 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
checked={Boolean(
checkedPermissions[
getRoleKey(permission)
]
],
)}
onChange={() =>
onPermissionChange(permission)
}
color="primary"
color='primary'
/>
}
label={permission.displayName}

View File

@ -73,15 +73,15 @@ export const RoleForm = ({
? getCategorizedProjectPermissions(
flattenProjectPermissions(
permissions.project,
permissions.environments
)
permissions.environments,
),
)
: getCategorizedRootPermissions(permissions.root);
const onPermissionChange = (permission: IPermission) => {
const newCheckedPermissions = togglePermission(
checkedPermissions,
permission
permission,
);
setCheckedPermissions(newCheckedPermissions);
};
@ -89,7 +89,7 @@ export const RoleForm = ({
const onCheckAll = (permissions: IPermission[]) => {
const newCheckedPermissions = toggleAllPermissions(
checkedPermissions,
permissions
permissions,
);
setCheckedPermissions(newCheckedPermissions);
@ -102,22 +102,22 @@ export const RoleForm = ({
</StyledInputDescription>
<StyledInput
autoFocus
label="Role name"
label='Role name'
error={Boolean(errors.name)}
errorText={errors.name}
value={name}
onChange={e => onSetName(e.target.value)}
autoComplete="off"
onChange={(e) => onSetName(e.target.value)}
autoComplete='off'
required
/>
<StyledInputDescription>
What is your new role description?
</StyledInputDescription>
<StyledInput
label="Role description"
label='Role description'
value={description}
onChange={e => setDescription(e.target.value)}
autoComplete="off"
onChange={(e) => setDescription(e.target.value)}
autoComplete='off'
required
/>
<StyledInputDescription>
@ -130,11 +130,11 @@ export const RoleForm = ({
context={label.toLowerCase()}
Icon={
type === PROJECT_PERMISSION_TYPE ? (
<TopicIcon color="disabled" sx={{ mr: 1 }} />
<TopicIcon color='disabled' sx={{ mr: 1 }} />
) : type === ENVIRONMENT_PERMISSION_TYPE ? (
<CloudCircleIcon color="disabled" sx={{ mr: 1 }} />
<CloudCircleIcon color='disabled' sx={{ mr: 1 }} />
) : (
<UserIcon color="disabled" sx={{ mr: 1 }} />
<UserIcon color='disabled' sx={{ mr: 1 }} />
)
}
permissions={permissions}

View File

@ -16,7 +16,7 @@ export interface IRoleFormErrors {
export const useRoleForm = (
initialName = '',
initialDescription = '',
initialPermissions: IPermission[] = []
initialPermissions: IPermission[] = [],
) => {
const { roles } = useRoles();
@ -45,7 +45,7 @@ export const useRoleForm = (
description,
type: type === ROOT_ROLE_TYPE ? 'root-custom' : 'custom',
permissions: Object.values(checkedPermissions).map(
({ name, environment }) => ({ name, environment })
({ name, environment }) => ({ name, environment }),
),
});
@ -53,7 +53,7 @@ export const useRoleForm = (
return !roles.some(
(existingRole: IRole) =>
existingRole.name !== initialName &&
existingRole.name.toLowerCase() === name.toLowerCase()
existingRole.name.toLowerCase() === name.toLowerCase(),
);
};
@ -63,18 +63,18 @@ export const useRoleForm = (
Object.keys(permissions).length > 0;
const clearError = (field: ErrorField) => {
setErrors(errors => ({ ...errors, [field]: undefined }));
setErrors((errors) => ({ ...errors, [field]: undefined }));
};
const setError = (field: ErrorField, error: string) => {
setErrors(errors => ({ ...errors, [field]: error }));
setErrors((errors) => ({ ...errors, [field]: error }));
};
const reload = () => {
setName(initialName);
setDescription(initialDescription);
setCheckedPermissions(
permissionsToCheckedPermissions(initialPermissions)
permissionsToCheckedPermissions(initialPermissions),
);
};

View File

@ -144,7 +144,7 @@ export const RoleModal = ({
? '#custom-root-roles'
: '#custom-project-roles'
}`}
documentationLinkLabel="Roles documentation"
documentationLinkLabel='Roles documentation'
formatApiCode={formatApiCode}
>
<StyledForm onSubmit={onSubmit}>
@ -160,9 +160,9 @@ export const RoleModal = ({
/>
<StyledButtonContainer>
<Button
type="submit"
variant="contained"
color="primary"
type='submit'
variant='contained'
color='primary'
disabled={!isValid}
>
{editing ? 'Save' : 'Add'} role

View File

@ -9,7 +9,7 @@ export const Roles = () => {
const { isEnterprise } = useUiConfig();
if (!isEnterprise()) {
return <PremiumFeature feature="project-roles" page />;
return <PremiumFeature feature='project-roles' page />;
}
return (

View File

@ -73,7 +73,7 @@ export const RolesPage = () => {
return (
<PageContent
withTabs
bodyClass="page-body"
bodyClass='page-body'
isLoading={loading}
header={
<>
@ -81,9 +81,9 @@ export const RolesPage = () => {
<StyledTabsContainer>
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
indicatorColor='primary'
textColor='primary'
variant='scrollable'
allowScrollButtonsMobile
>
{tabs.map(({ label, path, total }) => (
@ -120,7 +120,7 @@ export const RolesPage = () => {
setSelectedRole(undefined);
setModalOpen(true);
}}
maxWidth={`${theme.breakpoints.values['sm']}px`}
maxWidth={`${theme.breakpoints.values.sm}px`}
Icon={Add}
permission={ADMIN}
>
@ -142,7 +142,7 @@ export const RolesPage = () => {
>
<Routes>
<Route
path="project-roles"
path='project-roles'
element={
<RolesTable
type={PROJECT_ROLE_TYPE}
@ -155,7 +155,7 @@ export const RolesPage = () => {
}
/>
<Route
path="*"
path='*'
element={
<RolesTable
type={

View File

@ -32,22 +32,22 @@ export const RoleDeleteDialogProjectRole = ({
return (
<Dialogue
title="Delete project role?"
title='Delete project role?'
open={open}
primaryButtonText="Delete role"
secondaryButtonText="Cancel"
primaryButtonText='Delete role'
secondaryButtonText='Cancel'
disabledPrimaryButton={entitiesWithRole}
onClick={() => onConfirm(role!)}
onClose={() => {
setOpen(false);
}}
maxWidth="md"
maxWidth='md'
>
<ConditionallyRender
condition={entitiesWithRole}
show={
<>
<Alert severity="error">
<Alert severity='error'>
You are not allowed to delete a role that is
currently in use. Please change the role of the
following entities first:

View File

@ -63,7 +63,7 @@ export const RoleDeleteDialogProjectRoleTable = ({
maxWidth: 150,
},
] as Column<IProjectRoleUsageCount>[],
[]
[],
);
const { headerGroups, rows, prepareRow } = useTable(
@ -78,7 +78,7 @@ export const RoleDeleteDialogProjectRoleTable = ({
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
return (

View File

@ -56,7 +56,7 @@ export const RoleDeleteDialogGroups = ({
maxWidth: 150,
},
] as Column<IGroup>[],
[]
[],
);
const { headerGroups, rows, prepareRow } = useTable(
@ -71,7 +71,7 @@ export const RoleDeleteDialogGroups = ({
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
return (

View File

@ -36,20 +36,20 @@ export const RoleDeleteDialogRootRole = ({
const roleUsers = users.filter(({ rootRole }) => rootRole === role?.id);
const roleServiceAccounts = serviceAccounts.filter(
({ rootRole }) => rootRole === role?.id
({ rootRole }) => rootRole === role?.id,
);
const roleGroups = groups?.filter(({ rootRole }) => rootRole === role?.id);
const entitiesWithRole = Boolean(
roleUsers.length || roleServiceAccounts.length || roleGroups?.length
roleUsers.length || roleServiceAccounts.length || roleGroups?.length,
);
return (
<Dialogue
title="Delete root role?"
title='Delete root role?'
open={open}
primaryButtonText="Delete role"
secondaryButtonText="Cancel"
primaryButtonText='Delete role'
secondaryButtonText='Cancel'
disabledPrimaryButton={entitiesWithRole}
onClick={() => onConfirm(role!)}
onClose={() => {
@ -60,7 +60,7 @@ export const RoleDeleteDialogRootRole = ({
condition={entitiesWithRole}
show={
<>
<Alert severity="error">
<Alert severity='error'>
You are not allowed to delete a role that is
currently in use. Please change the role of the
following entities first:

View File

@ -81,7 +81,7 @@ export const RoleDeleteDialogServiceAccounts = ({
maxWidth: 150,
},
] as Column<IServiceAccount>[],
[]
[],
);
const { headerGroups, rows, prepareRow } = useTable(
@ -96,7 +96,7 @@ export const RoleDeleteDialogServiceAccounts = ({
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
return (

View File

@ -52,15 +52,15 @@ export const RoleDeleteDialogUsers = ({
Cell: ({ row: { original: user } }: any) => (
<TimeAgoCell
value={user.seenAt}
emptyText="Never"
title={date => `Last login: ${date}`}
emptyText='Never'
title={(date) => `Last login: ${date}`}
/>
),
sortType: 'date',
maxWidth: 150,
},
] as Column<IUser>[],
[]
[],
);
const { headerGroups, rows, prepareRow } = useTable(
@ -75,7 +75,7 @@ export const RoleDeleteDialogUsers = ({
disableMultiSort: true,
},
useSortBy,
useFlexLayout
useFlexLayout,
);
return (

View File

@ -20,7 +20,7 @@ export const RolesCell = ({ role }: IRolesCellProps) => (
afterTitle={
<ConditionallyRender
condition={PREDEFINED_ROLE_TYPES.includes(role.type)}
show={<StyledBadge color="success">Predefined</StyledBadge>}
show={<StyledBadge color='success'>Predefined</StyledBadge>}
/>
}
/>

View File

@ -70,7 +70,7 @@ export const RolesTable = ({
id: 'Icon',
Cell: () => (
<IconCell
icon={<SupervisedUserCircle color="disabled" />}
icon={<SupervisedUserCircle color='disabled' />}
/>
),
disableGlobalFilter: true,
@ -118,7 +118,7 @@ export const RolesTable = ({
searchable: true,
},
],
[]
[],
);
const [initialState] = useState({
@ -129,7 +129,7 @@ export const RolesTable = ({
const { data, getSearchText } = useSearch(
columns,
searchValue,
type === ROOT_ROLE_TYPE ? roles : projectRoles
type === ROOT_ROLE_TYPE ? roles : projectRoles,
);
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
@ -147,7 +147,7 @@ export const RolesTable = ({
},
},
useSortBy,
useFlexLayout
useFlexLayout,
);
useConditionallyHiddenColumns(
@ -158,7 +158,7 @@ export const RolesTable = ({
},
],
setHiddenColumns,
columns
columns,
);
return (

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