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.  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:
parent
751bc465d6
commit
4167a60588
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,7 +36,6 @@ unleash-server.tar.gz
|
||||
# Visual Studio Code
|
||||
jsconfig.json
|
||||
typings
|
||||
.vscode
|
||||
.nyc_output
|
||||
|
||||
# We use yarn.lock
|
||||
|
@ -1,2 +0,0 @@
|
||||
CHANGELOG.md
|
||||
website/docs
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
@ -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
|
||||
},
|
||||
|
@ -1,2 +0,0 @@
|
||||
.github/*
|
||||
CHANGELOG.md
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80
|
||||
}
|
@ -15,7 +15,7 @@ export default defineConfig({
|
||||
vitePreprocessor({
|
||||
configFile: path.resolve(__dirname, './vite.config.ts'),
|
||||
mode: 'development',
|
||||
})
|
||||
}),
|
||||
);
|
||||
on('task', {
|
||||
log(message) {
|
||||
|
2
frontend/cypress.d.ts
vendored
2
frontend/cypress.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {}
|
||||
type Chainable = {};
|
||||
}
|
||||
|
22
frontend/cypress/global.d.ts
vendored
22
frontend/cypress/global.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 %',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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%');
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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`,
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -14,7 +14,7 @@
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
// require('./commands')
|
||||
|
@ -5,5 +5,5 @@ const { version } = module.exports;
|
||||
|
||||
module.exports = {
|
||||
publicFolder: path.join(__dirname, 'build'),
|
||||
version
|
||||
version,
|
||||
};
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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 />;
|
||||
|
@ -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>{' '}
|
||||
|
@ -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>
|
||||
.
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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) => {
|
||||
|
@ -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')}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
@ -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>
|
||||
.
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -13,7 +13,7 @@ const FlaggedBillingRedirect = () => {
|
||||
return <InvoiceAdminPage />;
|
||||
}
|
||||
|
||||
return <Navigate to="/admin/billing" replace />;
|
||||
return <Navigate to='/admin/billing' replace />;
|
||||
};
|
||||
|
||||
export default FlaggedBillingRedirect;
|
||||
|
@ -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.
|
||||
};
|
||||
|
||||
|
@ -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>).
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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{' '}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>?
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -67,7 +67,7 @@ export const MaintenanceToggle = () => {
|
||||
<Switch
|
||||
onChange={updateEnabled}
|
||||
value={enabled}
|
||||
name="enabled"
|
||||
name='enabled'
|
||||
checked={enabled}
|
||||
/>
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -28,7 +28,7 @@ const MaintenancePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent header={<PageHeader title="Maintenance" />}>
|
||||
<PageContent header={<PageHeader title='Maintenance' />}>
|
||||
<StyledBox>
|
||||
<MaintenanceTooltip />
|
||||
<MaintenanceToggle />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
'"',
|
||||
'"'
|
||||
)}") -- ${reqs} req/s<br>${type} --> Unleash`
|
||||
'"',
|
||||
)}") -- ${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>}
|
||||
/>
|
||||
);
|
||||
|
@ -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.
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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),
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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={
|
||||
|
@ -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:
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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:
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user