diff --git a/app/src/common/constants/permissions.js b/app/src/common/constants/permissions.js index c9e5a4ffc4..9da13dbcbc 100644 --- a/app/src/common/constants/permissions.js +++ b/app/src/common/constants/permissions.js @@ -106,8 +106,8 @@ export const PERMISSIONS_MAP = { }, [MEMBER]: { [VIEWER]: { - [ACTIONS.SEE_SETTINGS]: true, - [ACTIONS.SEE_MEMBERS]: true, + [ACTIONS.SEE_SETTINGS]: false, + [ACTIONS.SEE_MEMBERS]: false, }, [EDITOR]: { [ACTIONS.SEE_SETTINGS]: true, diff --git a/app/src/common/urls.js b/app/src/common/urls.js index 34f574c55d..93d166f36c 100644 --- a/app/src/common/urls.js +++ b/app/src/common/urls.js @@ -129,6 +129,8 @@ export const URLS = { `${urlCommonBase}organizations${getQueryParams(preferencesObj)}`, organizationProjects: (organizationId, preferencesObj = {}) => `${urlCommonBase}organizations/${organizationId}/projects${getQueryParams(preferencesObj)}`, + organizationUsers: (organizationId, preferencesObj = {}) => + `${urlCommonBase}organizations/${organizationId}/users${getQueryParams(preferencesObj)}`, projectByName: (projectKey) => `${urlBase}project/${projectKey}`, project: (ids = []) => `${urlBase}project?ids=${ids.join(',')}`, diff --git a/app/src/componentLibrary/sidebar/sidebar.scss b/app/src/componentLibrary/sidebar/sidebar.scss index 4c385e7306..a88ab211a6 100644 --- a/app/src/componentLibrary/sidebar/sidebar.scss +++ b/app/src/componentLibrary/sidebar/sidebar.scss @@ -42,15 +42,20 @@ box-sizing: border-box; height: 100%; gap: 32px; - background-image: - linear-gradient(to right, $COLOR--darkmode-gray-500 0 48px,$COLOR--darkmode-gray-450 48px 100%); + background-image: linear-gradient( + to right, + $COLOR--darkmode-gray-500 0 48px, + $COLOR--darkmode-gray-450 48px 100% + ); z-index: 3; width: 328px; align-items: start; } @keyframes delay-overflow { - to { overflow: visible; } + to { + overflow: visible; + } } .items-block { diff --git a/app/src/componentLibrary/sidebar/sidebarButton/sidebarButton.scss b/app/src/componentLibrary/sidebar/sidebarButton/sidebarButton.scss index 50d9186537..1794be7d08 100644 --- a/app/src/componentLibrary/sidebar/sidebarButton/sidebarButton.scss +++ b/app/src/componentLibrary/sidebar/sidebarButton/sidebarButton.scss @@ -14,7 +14,7 @@ * limitations under the License. */ - .btn-icon { +.btn-icon { width: 48px; height: 40px; diff --git a/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsContent/twoStepsContent.scss b/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsContent/twoStepsContent.scss index 4b747cdf69..54a39612f5 100644 --- a/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsContent/twoStepsContent.scss +++ b/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsContent/twoStepsContent.scss @@ -28,7 +28,7 @@ padding: 24px 16px; background-color: $COLOR--bg-000; border-radius: 8px; - box-shadow: 0 1px 3px 0 rgba(55, 67, 98, 0.10); + box-shadow: 0 1px 3px 0 rgba(55, 67, 98, 0.1); } .step { @@ -54,6 +54,7 @@ } } -.scrollbars { // TODO will be replaced with new Scroll component in the future +.scrollbars { + // TODO will be replaced with new Scroll component in the future width: 376px !important; } diff --git a/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsFooter/twoStepsFooter.scss b/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsFooter/twoStepsFooter.scss index 6adf7937dd..0637173b8d 100644 --- a/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsFooter/twoStepsFooter.scss +++ b/app/src/components/integrations/modals/addLdapIntegrationModal/twoSteps/twoStepsFooter/twoStepsFooter.scss @@ -21,7 +21,8 @@ padding-top: 16px; } -.cancel-button, .discard-button { +.cancel-button, +.discard-button { margin-left: auto; } diff --git a/app/src/controllers/instance/organizations/index.js b/app/src/controllers/instance/organizations/index.js index c28ec3d2ef..b89052a05d 100644 --- a/app/src/controllers/instance/organizations/index.js +++ b/app/src/controllers/instance/organizations/index.js @@ -18,6 +18,7 @@ export { FETCH_ORGANIZATIONS } from './constants'; export { fetchOrganizationsAction } from './actionCreators'; export { organizationsReducer } from './reducer'; export { + organizationsSelector, organizationsListSelector, organizationsListLoadingSelector, organizationsListPaginationSelector, diff --git a/app/src/controllers/instance/organizations/sagas.js b/app/src/controllers/instance/organizations/sagas.js index c179fb1676..88dad58d28 100644 --- a/app/src/controllers/instance/organizations/sagas.js +++ b/app/src/controllers/instance/organizations/sagas.js @@ -18,8 +18,6 @@ import { takeEvery, all, put } from 'redux-saga/effects'; import { URLS } from 'common/urls'; import { showDefaultErrorNotification } from 'controllers/notification'; import { fetchDataAction } from 'controllers/fetch'; -import { organizationSagas } from 'controllers/organization'; -import { projectsSagas } from 'controllers/organization/projects'; import { FETCH_ORGANIZATIONS, NAMESPACE } from './constants'; function* fetchOrganizations() { @@ -35,5 +33,5 @@ function* watchFetchOrganizations() { } export function* organizationsSagas() { - yield all([watchFetchOrganizations(), organizationSagas(), projectsSagas()]); + yield all([watchFetchOrganizations()]); } diff --git a/app/src/controllers/organization/projects/index.js b/app/src/controllers/organization/projects/index.js index 4d223dc072..f16697a714 100644 --- a/app/src/controllers/organization/projects/index.js +++ b/app/src/controllers/organization/projects/index.js @@ -16,12 +16,7 @@ export { fetchOrganizationProjectsAction, navigateToProjectAction } from './actionCreators'; export { projectsReducer } from './reducer'; -export { - projectsPaginationSelector, - projectsSelector, - loadingSelector, - querySelector, -} from './selectors'; +export { projectsPaginationSelector, projectsSelector, loadingSelector } from './selectors'; export { projectsSagas } from './sagas'; export { DEFAULT_LIMITATION, diff --git a/app/src/controllers/organization/projects/sagas.js b/app/src/controllers/organization/projects/sagas.js index 33e45c09ed..c0a6af46ea 100644 --- a/app/src/controllers/organization/projects/sagas.js +++ b/app/src/controllers/organization/projects/sagas.js @@ -21,10 +21,9 @@ import { fetch } from 'common/utils'; import { hideModalAction } from 'controllers/modal'; import { NOTIFICATION_TYPES, showNotification } from 'controllers/notification'; import { fetchOrganizationBySlugAction } from '..'; -import { activeOrganizationSelector } from '../selectors'; +import { activeOrganizationSelector, querySelector } from '../selectors'; import { fetchOrganizationProjectsAction } from './actionCreators'; -import { querySelector } from './selectors'; -import { CREATE_PROJECT, ERROR_CODES, FETCH_ORGANIZATION_PROJECTS, NAMESPACE } from './constants'; +import { CREATE_PROJECT, FETCH_ORGANIZATION_PROJECTS, ERROR_CODES, NAMESPACE } from './constants'; function* fetchOrganizationProjects({ payload: organizationId }) { const query = yield select(querySelector); diff --git a/app/src/controllers/organization/projects/selectors.js b/app/src/controllers/organization/projects/selectors.js index 3f450eab23..243cf099ec 100644 --- a/app/src/controllers/organization/projects/selectors.js +++ b/app/src/controllers/organization/projects/selectors.js @@ -14,11 +14,6 @@ * limitations under the License. */ -import { createSelector } from 'reselect'; -import { createQueryParametersSelector } from 'controllers/pages'; -import { SORTING_ASC } from 'controllers/sorting'; -import { getAlternativePaginationAndSortParams, PAGE_KEY, SIZE_KEY } from 'controllers/pagination'; -import { SORTING_KEY, DEFAULT_PAGINATION } from './constants'; import { organizationSelector } from '../selectors'; const domainSelector = (state) => organizationSelector(state).projects || {}; @@ -26,25 +21,3 @@ const domainSelector = (state) => organizationSelector(state).projects || {}; export const projectsPaginationSelector = (state) => domainSelector(state).pagination; export const projectsSelector = (state) => domainSelector(state).projects; export const loadingSelector = (state) => domainSelector(state).loading || false; - -export const createOrganizationProjectsParametersSelector = ({ - defaultPagination, - defaultSorting, - sortingKey, -} = {}) => - createSelector( - createQueryParametersSelector({ - defaultPagination, - defaultSorting, - sortingKey, - }), - ({ [SIZE_KEY]: limit, [SORTING_KEY]: sort, [PAGE_KEY]: pageNumber, ...rest }) => { - return { ...getAlternativePaginationAndSortParams(sort, limit, pageNumber), ...rest }; - }, - ); - -export const querySelector = createOrganizationProjectsParametersSelector({ - defaultPagination: DEFAULT_PAGINATION, - defaultDirection: SORTING_ASC, - sortingKey: SORTING_KEY, -}); diff --git a/app/src/controllers/organization/reducer.js b/app/src/controllers/organization/reducer.js index c8514b38a3..1bfe3f90f6 100644 --- a/app/src/controllers/organization/reducer.js +++ b/app/src/controllers/organization/reducer.js @@ -19,6 +19,7 @@ import { fetchReducer } from 'controllers/fetch'; import { loadingReducer } from 'controllers/loading'; import { queueReducers } from 'common/utils'; import { projectsReducer } from './projects/reducer'; +import { usersReducer } from './users/reducer'; import { FETCH_ORGANIZATION_BY_SLUG, SET_ACTIVE_ORGANIZATION } from './constants'; const setActiveOrganizationReducer = (state = [], { type = '', payload = {} }) => { @@ -41,4 +42,5 @@ export const organizationReducer = combineReducers({ ), organizationLoading: loadingReducer(FETCH_ORGANIZATION_BY_SLUG), projects: projectsReducer, + users: usersReducer, }); diff --git a/app/src/controllers/organization/sagas.js b/app/src/controllers/organization/sagas.js index 68c0e6dbdf..3fedbdfbf2 100644 --- a/app/src/controllers/organization/sagas.js +++ b/app/src/controllers/organization/sagas.js @@ -20,9 +20,10 @@ import { redirect } from 'redux-first-router'; import { ORGANIZATIONS_PAGE } from 'controllers/pages'; import { URLS } from 'common/urls'; import { showDefaultErrorNotification } from 'controllers/notification'; -import { fetchOrganizationProjectsAction } from 'controllers/organization/projects'; +import { fetchOrganizationProjectsAction, projectsSagas } from './projects'; import { FETCH_ORGANIZATION_BY_SLUG, PREPARE_ACTIVE_ORGANIZATION_PROJECTS } from './constants'; import { activeOrganizationSelector } from './selectors'; +import { usersSagas } from './users'; function* fetchOrganizationBySlug({ payload: slug }) { try { @@ -58,5 +59,10 @@ function* watchFetchOrganizationBySlug() { } export function* organizationSagas() { - yield all([watchFetchOrganizationProjects(), watchFetchOrganizationBySlug()]); + yield all([ + watchFetchOrganizationProjects(), + watchFetchOrganizationBySlug(), + projectsSagas(), + usersSagas(), + ]); } diff --git a/app/src/controllers/organization/selectors.js b/app/src/controllers/organization/selectors.js index 10269b7e36..af40e6cef9 100644 --- a/app/src/controllers/organization/selectors.js +++ b/app/src/controllers/organization/selectors.js @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { createSelector } from 'reselect'; +import { createQueryParametersSelector } from 'controllers/pages'; +import { getAlternativePaginationAndSortParams, PAGE_KEY, SIZE_KEY } from 'controllers/pagination'; +import { SORTING_ASC } from 'controllers/sorting'; import { organizationsSelector } from 'controllers/instance/organizations/selectors'; +import { SORTING_KEY } from './projects'; +import { DEFAULT_PAGINATION } from './projects/constants'; export const organizationSelector = (state) => organizationsSelector(state).organization || {}; @@ -26,3 +31,21 @@ export const activeOrganizationLoadingSelector = (state) => export const activeOrganizationNameSelector = (state) => activeOrganizationSelector(state)?.name; export const activeOrganizationIdSelector = (state) => activeOrganizationSelector(state)?.id; + +export const createParametersSelector = ({ defaultPagination, defaultSorting, sortingKey } = {}) => + createSelector( + createQueryParametersSelector({ + defaultPagination, + defaultSorting, + sortingKey, + }), + ({ [SIZE_KEY]: limit, [SORTING_KEY]: sort, [PAGE_KEY]: pageNumber, ...rest }) => { + return { ...getAlternativePaginationAndSortParams(sort, limit, pageNumber), ...rest }; + }, + ); + +export const querySelector = createParametersSelector({ + defaultPagination: DEFAULT_PAGINATION, + defaultDirection: SORTING_ASC, + sortingKey: SORTING_KEY, +}); diff --git a/app/src/controllers/organization/users/actionCreators.js b/app/src/controllers/organization/users/actionCreators.js new file mode 100644 index 0000000000..7cd1914e82 --- /dev/null +++ b/app/src/controllers/organization/users/actionCreators.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FETCH_ORGANIZATION_USERS, PREPARE_ACTIVE_ORGANIZATION_USERS } from './constants'; + +export const prepareActiveOrganizationUsersAction = (payload) => ({ + type: PREPARE_ACTIVE_ORGANIZATION_USERS, + payload, +}); + +export const fetchOrganizationUsersAction = (params) => { + return { + type: FETCH_ORGANIZATION_USERS, + payload: params, + }; +}; diff --git a/app/src/controllers/organization/users/constants.js b/app/src/controllers/organization/users/constants.js new file mode 100644 index 0000000000..0a688fd9dd --- /dev/null +++ b/app/src/controllers/organization/users/constants.js @@ -0,0 +1,19 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FETCH_ORGANIZATION_USERS = 'fetchOrganizationUsers'; +export const PREPARE_ACTIVE_ORGANIZATION_USERS = 'prepareActiveOrganizationUsers'; +export const NAMESPACE = 'organizationUsers'; diff --git a/app/src/controllers/organization/users/index.js b/app/src/controllers/organization/users/index.js new file mode 100644 index 0000000000..18b3dd219b --- /dev/null +++ b/app/src/controllers/organization/users/index.js @@ -0,0 +1,23 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { NAMESPACE, FETCH_ORGANIZATION_USERS } from './constants'; +export { + prepareActiveOrganizationUsersAction, + fetchOrganizationUsersAction, +} from './actionCreators'; +export { usersPaginationSelector, usersSelector, loadingSelector } from './selectors'; +export { usersSagas } from './sagas'; diff --git a/app/src/controllers/organization/users/reducer.js b/app/src/controllers/organization/users/reducer.js new file mode 100644 index 0000000000..ce7daa57bf --- /dev/null +++ b/app/src/controllers/organization/users/reducer.js @@ -0,0 +1,37 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { combineReducers } from 'redux'; +import { fetchReducer } from 'controllers/fetch'; +import { alternativePaginationReducer } from 'controllers/pagination'; +import { loadingReducer } from 'controllers/loading'; +import { createPageScopedReducer } from 'common/utils/createPageScopedReducer'; +import { ORGANIZATION_USERS_PAGE } from 'controllers/pages/constants'; +import { NAMESPACE } from './constants'; +import { initialPaginationState } from '../projects/constants'; + +export const usersFetchReducer = fetchReducer(NAMESPACE, { + contentPath: 'items', + initialState: [], +}); + +export const reducer = combineReducers({ + pagination: alternativePaginationReducer(NAMESPACE, initialPaginationState), + loading: loadingReducer(NAMESPACE), + users: usersFetchReducer, +}); + +export const usersReducer = createPageScopedReducer(reducer, ORGANIZATION_USERS_PAGE); diff --git a/app/src/controllers/organization/users/sagas.js b/app/src/controllers/organization/users/sagas.js new file mode 100644 index 0000000000..f3d8d4d534 --- /dev/null +++ b/app/src/controllers/organization/users/sagas.js @@ -0,0 +1,58 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createFetchPredicate, fetchDataAction } from 'controllers/fetch'; +import { URLS } from 'common/urls'; +import { all, put, select, take, takeEvery } from 'redux-saga/effects'; +import { activeOrganizationSelector, querySelector } from '../selectors'; +import { FETCH_ORGANIZATION_BY_SLUG } from '../constants'; +import { fetchOrganizationUsersAction } from './actionCreators'; +import { + FETCH_ORGANIZATION_USERS, + NAMESPACE, + PREPARE_ACTIVE_ORGANIZATION_USERS, +} from './constants'; + +function* fetchOrganizationUsers({ payload: organizationId }) { + const query = yield select(querySelector); + + yield put(fetchDataAction(NAMESPACE)(URLS.organizationUsers(organizationId, { ...query }))); +} + +function* watchFetchUsers() { + yield takeEvery(FETCH_ORGANIZATION_USERS, fetchOrganizationUsers); +} + +function* prepareActiveOrganizationUsers({ payload: { organizationSlug } }) { + let activeOrganization = yield select(activeOrganizationSelector); + try { + if (!activeOrganization || organizationSlug !== activeOrganization?.slug) { + yield take(createFetchPredicate(FETCH_ORGANIZATION_BY_SLUG)); + activeOrganization = yield select(activeOrganizationSelector); + } + yield put(fetchOrganizationUsersAction(activeOrganization.id)); + } catch (error) { + throw new Error(error); + } +} + +function* watchFetchOrganizationUsers() { + yield takeEvery(PREPARE_ACTIVE_ORGANIZATION_USERS, prepareActiveOrganizationUsers); +} + +export function* usersSagas() { + yield all([watchFetchUsers(), watchFetchOrganizationUsers()]); +} diff --git a/app/src/controllers/organization/users/selectors.js b/app/src/controllers/organization/users/selectors.js new file mode 100644 index 0000000000..f7df8911fe --- /dev/null +++ b/app/src/controllers/organization/users/selectors.js @@ -0,0 +1,23 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { organizationSelector } from '../selectors'; + +const domainSelector = (state) => organizationSelector(state).users || {}; + +export const usersPaginationSelector = (state) => domainSelector(state).pagination; +export const usersSelector = (state) => domainSelector(state).users; +export const loadingSelector = (state) => domainSelector(state).loading || false; diff --git a/app/src/controllers/pages/constants.js b/app/src/controllers/pages/constants.js index 845842861d..11b8daf1ce 100644 --- a/app/src/controllers/pages/constants.js +++ b/app/src/controllers/pages/constants.js @@ -32,7 +32,7 @@ export const PLUGIN_UI_EXTENSION_ADMIN_PAGE = 'PLUGIN_UI_EXTENSION_ADMIN_PAGE'; export const API_PAGE = 'API_PAGE'; export const ORGANIZATIONS_PAGE = 'ORGANIZATIONS_PAGE'; export const ORGANIZATION_PROJECTS_PAGE = 'ORGANIZATION_PROJECTS_PAGE'; -export const ORGANIZATION_MEMBERS_PAGE = 'ORGANIZATION_MEMBERS_PAGE'; +export const ORGANIZATION_USERS_PAGE = 'ORGANIZATION_USERS_PAGE'; export const ORGANIZATION_SETTINGS_PAGE = 'ORGANIZATION_SETTINGS_PAGE'; export const PROJECT_PAGE = 'PROJECT_PAGE'; export const PROJECT_DASHBOARD_PAGE = 'PROJECT_DASHBOARD_PAGE'; @@ -72,6 +72,7 @@ export const pageNames = { [NOT_FOUND]: NOT_FOUND, ORGANIZATIONS_PAGE, ORGANIZATION_PROJECTS_PAGE, + ORGANIZATION_USERS_PAGE, ALL_USERS_PAGE, SERVER_SETTINGS_PAGE, SERVER_SETTINGS_TAB_PAGE, diff --git a/app/src/controllers/pages/index.js b/app/src/controllers/pages/index.js index a9540b4d42..10c6924627 100644 --- a/app/src/controllers/pages/index.js +++ b/app/src/controllers/pages/index.js @@ -92,7 +92,7 @@ export { PROJECT_PLUGIN_PAGE, ORGANIZATIONS_PAGE, ORGANIZATION_PROJECTS_PAGE, - ORGANIZATION_MEMBERS_PAGE, + ORGANIZATION_USERS_PAGE, ORGANIZATION_SETTINGS_PAGE, } from './constants'; export { NOT_FOUND } from 'redux-first-router'; diff --git a/app/src/layouts/organizationLayout/organizationSidebar/organizationSidebar.jsx b/app/src/layouts/organizationLayout/organizationSidebar/organizationSidebar.jsx index 00c82d7075..81152fc421 100644 --- a/app/src/layouts/organizationLayout/organizationSidebar/organizationSidebar.jsx +++ b/app/src/layouts/organizationLayout/organizationSidebar/organizationSidebar.jsx @@ -23,7 +23,7 @@ import { useIntl } from 'react-intl'; import { canSeeMembers } from 'common/utils/permissions'; import { ORGANIZATION_PROJECTS_PAGE, - ORGANIZATION_MEMBERS_PAGE, + ORGANIZATION_USERS_PAGE, ORGANIZATION_SETTINGS_PAGE, USER_PROFILE_PAGE_ORGANIZATION_LEVEL, ORGANIZATIONS_PAGE, @@ -74,7 +74,7 @@ export const OrganizationSidebar = ({ onClickNavBtn }) => { onClick: (isSidebarCollapsed) => onClickButton({ itemName: messages.users.defaultMessage, isSidebarCollapsed }), link: { - type: ORGANIZATION_MEMBERS_PAGE, + type: ORGANIZATION_USERS_PAGE, payload: { organizationSlug }, }, icon: MembersIcon, diff --git a/app/src/pages/inside/common/statusDropdown/statusDropdown.scss b/app/src/pages/inside/common/statusDropdown/statusDropdown.scss index 6e7c8f08b9..a0c121a536 100644 --- a/app/src/pages/inside/common/statusDropdown/statusDropdown.scss +++ b/app/src/pages/inside/common/statusDropdown/statusDropdown.scss @@ -68,4 +68,4 @@ .defined-status { padding: 0 16px; -} \ No newline at end of file +} diff --git a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/clusterItemsGridRow/clusterItemsGridRow.scss b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/clusterItemsGridRow/clusterItemsGridRow.scss index 2393135995..27640a8260 100644 --- a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/clusterItemsGridRow/clusterItemsGridRow.scss +++ b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/clusterItemsGridRow/clusterItemsGridRow.scss @@ -36,7 +36,7 @@ } &.extension-col { - width: 165px; + width: 165px; } } diff --git a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss index 3a41f6aa38..98fd576166 100644 --- a/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss +++ b/app/src/pages/inside/uniqueErrorsPage/uniqueErrorsGrid/uniqueErrorsGrid.scss @@ -22,7 +22,7 @@ padding-left: 37px; } -.matched-header{ +.matched-header { width: 130px; } diff --git a/app/src/pages/instance/organizationsPage/organizationsPage.scss b/app/src/pages/instance/organizationsPage/organizationsPage.scss index 2c8dbc2c44..774adb7b35 100644 --- a/app/src/pages/instance/organizationsPage/organizationsPage.scss +++ b/app/src/pages/instance/organizationsPage/organizationsPage.scss @@ -14,7 +14,7 @@ * limitations under the License. */ - .organizations-page { +.organizations-page { display: flex; flex-direction: column; min-height: 100%; diff --git a/app/src/pages/instance/projectEventsPage/eventsGrid/eventsGrid.scss b/app/src/pages/instance/projectEventsPage/eventsGrid/eventsGrid.scss index 7af8f8ac6d..074145bbf6 100644 --- a/app/src/pages/instance/projectEventsPage/eventsGrid/eventsGrid.scss +++ b/app/src/pages/instance/projectEventsPage/eventsGrid/eventsGrid.scss @@ -74,7 +74,7 @@ .events-grid-row { @media (max-width: $SCREEN_SM_MAX) { display: table-row; - > :nth-last-child(-n+2) { + > :nth-last-child(-n + 2) { max-width: 0; display: none; } diff --git a/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.jsx b/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.jsx new file mode 100644 index 0000000000..23825e4cd7 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.jsx @@ -0,0 +1,40 @@ +import { useIntl } from 'react-intl'; +import { BubblesLoader } from '@reportportal/ui-kit'; +import PropTypes from 'prop-types'; +import classNames from 'classnames/bind'; +import { EmptyPageState } from 'pages/common/emptyPageState'; +import { messages } from '../membersPageHeader/messages'; +import EmptyIcon from './img/empty-members-icon-inline.svg'; +import styles from './emptyMembersPageState.scss'; + +const cx = classNames.bind(styles); + +export const EmptyMembersPageState = ({ isLoading, hasPermission, showInviteUserModal }) => { + const { formatMessage } = useIntl(); + return isLoading ? ( +
+ +
+ ) : ( + + ); +}; + +EmptyMembersPageState.propTypes = { + isLoading: PropTypes.bool, + hasPermission: PropTypes.bool, + showInviteUserModal: PropTypes.func, +}; + +EmptyMembersPageState.defaultProps = { + isLoading: false, + hasPermission: false, + showInviteUserModal: () => {}, +}; diff --git a/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.scss b/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.scss new file mode 100644 index 0000000000..c33c8fb561 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/emptyMembersPageState/emptyMembersPageState.scss @@ -0,0 +1,7 @@ +.loader { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex: 1; +} diff --git a/app/src/pages/organization/projectTeamPage/img/empty-members-icon-inline.svg b/app/src/pages/organization/common/membersPage/emptyMembersPageState/img/empty-members-icon-inline.svg similarity index 100% rename from app/src/pages/organization/projectTeamPage/img/empty-members-icon-inline.svg rename to app/src/pages/organization/common/membersPage/emptyMembersPageState/img/empty-members-icon-inline.svg diff --git a/app/src/pages/organization/common/membersPage/emptyMembersPageState/index.js b/app/src/pages/organization/common/membersPage/emptyMembersPageState/index.js new file mode 100644 index 0000000000..21692e2098 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/emptyMembersPageState/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EmptyMembersPageState } from './emptyMembersPageState'; diff --git a/app/src/pages/organization/common/membersPage/membersListTable/index.js b/app/src/pages/organization/common/membersPage/membersListTable/index.js new file mode 100644 index 0000000000..416b8d5229 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersListTable/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MembersListTable } from './membersListTable'; diff --git a/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.jsx b/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.jsx new file mode 100644 index 0000000000..7370f3447d --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.jsx @@ -0,0 +1,81 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames/bind'; +import PropTypes from 'prop-types'; +import { Table } from '@reportportal/ui-kit'; +import { DEFAULT_PAGE_SIZE_OPTIONS } from 'controllers/members/constants'; +import { PaginationWrapper } from 'components/main/paginationWrapper'; +import styles from './membersListTable.scss'; + +const cx = classNames.bind(styles); + +export const MembersListTable = ({ + data, + primaryColumn, + fixedColumns, + onTableSorting, + showPagination, + rowActionMenu, + sortingDirection, + pageSize, + activePage, + itemCount, + pageCount, + onChangePage, + onChangePageSize, +}) => { + return ( + + + + ); +}; + +MembersListTable.propTypes = { + data: PropTypes.array.isRequired, + primaryColumn: PropTypes.object.isRequired, + fixedColumns: PropTypes.array.isRequired, + onTableSorting: PropTypes.func.isRequired, + showPagination: PropTypes.bool.isRequired, + rowActionMenu: PropTypes.node, + sortingDirection: PropTypes.string.isRequired, + pageSize: PropTypes.number.isRequired, + activePage: PropTypes.number.isRequired, + itemCount: PropTypes.number.isRequired, + pageCount: PropTypes.number.isRequired, + onChangePage: PropTypes.func.isRequired, + onChangePageSize: PropTypes.func.isRequired, +}; diff --git a/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.scss b/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.scss new file mode 100644 index 0000000000..676f59a1ec --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersListTable/membersListTable.scss @@ -0,0 +1,22 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.members-list-table { + margin-top: 24px; + padding: 0 32px 32px 32px; + max-width: 1264px; + box-sizing: border-box; +} diff --git a/app/src/pages/organization/common/membersPage/membersPageHeader/index.js b/app/src/pages/organization/common/membersPage/membersPageHeader/index.js new file mode 100644 index 0000000000..ae997aaaa5 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersPageHeader/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MembersPageHeader } from './membersPageHeader'; diff --git a/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.jsx b/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.jsx new file mode 100644 index 0000000000..61b598ec47 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.jsx @@ -0,0 +1,42 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames/bind'; +import styles from './membersPageHeader.scss'; + +const cx = classNames.bind(styles); + +export const MembersPageHeader = ({ title, children }) => { + return ( +
+
+ {title} + {children} +
+
+ ); +}; + +MembersPageHeader.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node, +}; + +MembersPageHeader.defaultProps = { + children: null, +}; diff --git a/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.scss b/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.scss new file mode 100644 index 0000000000..0ab16a39b4 --- /dev/null +++ b/app/src/pages/organization/common/membersPage/membersPageHeader/membersPageHeader.scss @@ -0,0 +1,42 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.members-page-header-container { + padding: 48px 32px 16px 32px; + border-bottom: 1px solid $COLOR--e-100; + background: $COLOR--bg-000; + box-sizing: border-box; + position: sticky; + top: 0; + z-index: 2; +} + +.header { + display: flex; + min-height: 31px; + justify-content: space-between; +} + +.title { + font-family: $FONT-REGULAR; + font-size: 20px; + line-height: 31px; + color: $COLOR--almost-black; + text-transform: capitalize; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/src/pages/organization/projectTeamPage/messages.js b/app/src/pages/organization/common/membersPage/membersPageHeader/messages.js similarity index 88% rename from app/src/pages/organization/projectTeamPage/messages.js rename to app/src/pages/organization/common/membersPage/membersPageHeader/messages.js index c6fb7c4840..386bf2fa28 100644 --- a/app/src/pages/organization/projectTeamPage/messages.js +++ b/app/src/pages/organization/common/membersPage/membersPageHeader/messages.js @@ -17,7 +17,11 @@ import { defineMessages } from 'react-intl'; export const messages = defineMessages({ - title: { + organizationUsersTitle: { + id: 'OrganizationUsers.organizationUsersTitle', + defaultMessage: 'Organization users', + }, + projectTeamTitle: { id: 'ProjectTeamPage.title', defaultMessage: 'Project team', }, diff --git a/app/src/pages/organization/projectTeamPage/projectTeamListTable/messages.js b/app/src/pages/organization/common/membersPage/messages.js similarity index 86% rename from app/src/pages/organization/projectTeamPage/projectTeamListTable/messages.js rename to app/src/pages/organization/common/membersPage/messages.js index 6dee0dea73..608d0e4677 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamListTable/messages.js +++ b/app/src/pages/organization/common/membersPage/messages.js @@ -33,4 +33,12 @@ export const messages = defineMessages({ id: 'MembersListTable.permissions', defaultMessage: 'Permissions', }, + role: { + id: 'MembersListTable.role', + defaultMessage: 'Role', + }, + projects: { + id: 'MembersListTable.projects', + defaultMessage: 'Projects', + }, }); diff --git a/app/src/pages/organization/organizationProjectsPage/projectsListTable/projectsListTable.scss b/app/src/pages/organization/organizationProjectsPage/projectsListTable/projectsListTable.scss index 05a57970b4..7bd4d241f4 100644 --- a/app/src/pages/organization/organizationProjectsPage/projectsListTable/projectsListTable.scss +++ b/app/src/pages/organization/organizationProjectsPage/projectsListTable/projectsListTable.scss @@ -66,6 +66,6 @@ } } -.loader{ +.loader { margin: auto; } diff --git a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.scss b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.scss index a905189575..617f2e7646 100644 --- a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.scss +++ b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.scss @@ -14,7 +14,7 @@ * limitations under the License. */ - .projects-page-header-container { +.projects-page-header-container { padding: 16px 32px; border-bottom: 1px solid $COLOR--e-100; background: $COLOR--bg-000; @@ -100,7 +100,8 @@ } } - .details-item-icon, svg { + .details-item-icon, + svg { width: 16px; height: 16px; } @@ -108,4 +109,4 @@ .search-input { overflow: hidden; -} \ No newline at end of file +} diff --git a/app/src/pages/organization/organizationUsersPage/index.js b/app/src/pages/organization/organizationUsersPage/index.js new file mode 100644 index 0000000000..102ff7d3b9 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OrganizationUsersPage } from './organizationUsersPage'; diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/index.js b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/index.js new file mode 100644 index 0000000000..46d3945561 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OrganizationTeamListTable } from './organizationUsersListTable'; diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.jsx b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.jsx new file mode 100644 index 0000000000..2b2525ec32 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.jsx @@ -0,0 +1,205 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames/bind'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { AbsRelTime } from 'components/main/absRelTime'; +import { MeatballMenuIcon, Popover } from '@reportportal/ui-kit'; +import { urlOrganizationAndProjectSelector } from 'controllers/pages'; +import { SORTING_ASC, withSortingURL } from 'controllers/sorting'; +import { DEFAULT_SORT_COLUMN } from 'controllers/members/constants'; +import { + DEFAULT_PAGE_SIZE, + DEFAULT_PAGINATION, + PAGE_KEY, + withPagination, +} from 'controllers/pagination'; +import { + prepareActiveOrganizationUsersAction, + usersPaginationSelector, +} from 'controllers/organization/users'; +import { SORTING_KEY } from 'controllers/organization/projects'; +import { ADMINISTRATOR } from 'common/constants/accountRoles'; +import { MembersListTable } from '../../common/membersPage/membersListTable'; +import { messages } from '../../common/membersPage/messages'; +import styles from './organizationUsersListTable.scss'; + +const cx = classNames.bind(styles); + +const OrgTeamListTableWrapped = ({ + users, + onChangeSorting, + sortingDirection, + pageSize, + activePage, + itemCount, + pageCount, + onChangePage, + onChangePageSize, +}) => { + const { formatMessage } = useIntl(); + const dispatch = useDispatch(); + const { organizationSlug, projectSlug } = useSelector(urlOrganizationAndProjectSelector); + const showPagination = users.length > 0; + const data = useMemo( + () => + users.map( + ({ + id, + email, + full_name: fullName, + relationships, + instance_role: instanceRole, + last_login_at: lastLogin, + org_role: orgRole, + }) => { + const projectsCount = relationships.projects.meta.count; + return { + id, + fullName: { + content: fullName, + component: ( +
+
{fullName}
+ {instanceRole === ADMINISTRATOR && ( +
+ +
+ )} +
+ ), + }, + email, + lastLogin: { + content: lastLogin, + component: lastLogin ? ( + + ) : ( + n/a + ), + }, + permissions: orgRole, + projects: projectsCount, + }; + }, + ), + [users, organizationSlug, projectSlug], + ); + + const primaryColumn = { + key: 'fullName', + header: formatMessage(messages.name), + }; + + const fixedColumns = useMemo( + () => [ + { + key: 'email', + header: formatMessage(messages.email), + width: 208, + align: 'left', + }, + { + key: 'lastLogin', + header: formatMessage(messages.lastLogin), + width: 156, + align: 'left', + }, + { + key: 'permissions', + header: formatMessage(messages.role), + width: 114, + align: 'left', + }, + { + key: 'projects', + header: formatMessage(messages.projects), + width: 104, + align: 'right', + }, + ], + [formatMessage], + ); + + const rowActionMenu = ( + +

Manage assignments

+ + } + > + + + +
+ ); + + const onTableSorting = ({ key }) => { + onChangeSorting(key); + dispatch(prepareActiveOrganizationUsersAction()); + }; + + return ( + + ); +}; + +OrgTeamListTableWrapped.propTypes = { + users: PropTypes.array, + sortingDirection: PropTypes.string, + onChangeSorting: PropTypes.func, + pageSize: PropTypes.number, + activePage: PropTypes.number, + itemCount: PropTypes.number.isRequired, + pageCount: PropTypes.number.isRequired, + onChangePage: PropTypes.func.isRequired, + onChangePageSize: PropTypes.func.isRequired, +}; + +OrgTeamListTableWrapped.defaultProps = { + users: [], + pageSize: DEFAULT_PAGE_SIZE, + activePage: DEFAULT_PAGINATION[PAGE_KEY], +}; + +export const OrganizationTeamListTable = withSortingURL({ + defaultFields: [DEFAULT_SORT_COLUMN], + defaultDirection: SORTING_ASC, + sortingKey: SORTING_KEY, +})( + withPagination({ + paginationSelector: usersPaginationSelector, + })(OrgTeamListTableWrapped), +); diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.scss b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.scss new file mode 100644 index 0000000000..b3d523bd01 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable.scss @@ -0,0 +1,61 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.row-action-menu { + display: flex; + flex-direction: column; + + p { + padding: 7px 16px; + align-items: center; + font-size: 13px; + line-height: 20px; + width: 120px; + color: $COLOR--almost-black; + } +} + +.menu-icon { + svg { + margin-bottom: 3px; + } +} + +.admin-badge { + display: inline-flex; + justify-content: center; + align-items: center; + padding: 2px 8px; + border-radius: 6px; + background-color: $COLOR--tag-value-text; + color: $COLOR--white-two; + font-family: $FONT-ROBOTO-BOLD; + font-size: 11px; + line-height: 16px; + margin-right: 16px; +} + +.member-name-column { + display: flex; + align-items: center; + gap: 16px; +} + +.date { + font-family: inherit; + color: inherit; + min-height: auto; +} diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersPage.jsx b/app/src/pages/organization/organizationUsersPage/organizationUsersPage.jsx new file mode 100644 index 0000000000..5e8f000809 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersPage.jsx @@ -0,0 +1,49 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useSelector } from 'react-redux'; +import { loadingSelector, usersSelector } from 'controllers/organization/users'; +import { OrganizationTeamListTable } from 'pages/organization/organizationUsersPage/organizationUsersListTable/organizationUsersListTable'; +import { ScrollWrapper } from 'components/main/scrollWrapper'; +import classNames from 'classnames/bind'; +import styles from './organizationUsersPage.scss'; +import { EmptyMembersPageState as EmptyUsersPageState } from '../common/membersPage/emptyMembersPageState'; +import { OrganizationUsersPageHeader } from './organizationUsersPageHeader'; + +const cx = classNames.bind(styles); + +export const OrganizationUsersPage = () => { + const users = useSelector(usersSelector); + const isUsersLoading = useSelector(loadingSelector); + const isEmptyUsers = users.length === 0; + + return ( + +
+ + {isEmptyUsers ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersPage.scss b/app/src/pages/organization/organizationUsersPage/organizationUsersPage.scss new file mode 100644 index 0000000000..eea90a59cf --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersPage.scss @@ -0,0 +1,21 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.organization-users-page { + display: flex; + flex-direction: column; + min-height: calc(100% - 48px); +} diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/index.js b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/index.js new file mode 100644 index 0000000000..c4ced6e20f --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OrganizationUsersPageHeader } from './organizationUsersPageHeader'; diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.jsx b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.jsx new file mode 100644 index 0000000000..de1084197f --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.jsx @@ -0,0 +1,59 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames/bind'; +import { Button, SearchIcon } from '@reportportal/ui-kit'; +import { useIntl } from 'react-intl'; +import { messages } from '../../common/membersPage/membersPageHeader/messages'; +import styles from './organizationUsersPageHeader.scss'; +import { MembersPageHeader } from '../../common/membersPage/membersPageHeader'; + +const cx = classNames.bind(styles); + +export const OrganizationUsersPageHeader = ({ isNotEmpty, onInvite }) => { + const { formatMessage } = useIntl(); + + return ( + +
+ {isNotEmpty && ( + <> +
+ + + +
+ + + )} +
+
+ ); +}; + +OrganizationUsersPageHeader.propTypes = { + isNotEmpty: PropTypes.bool, + onInvite: PropTypes.func, +}; + +OrganizationUsersPageHeader.defaultProps = { + isNotEmpty: false, + onInvite: () => {}, +}; diff --git a/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.scss b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.scss new file mode 100644 index 0000000000..162e23ef69 --- /dev/null +++ b/app/src/pages/organization/organizationUsersPage/organizationUsersPageHeader/organizationUsersPageHeader.scss @@ -0,0 +1,45 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.actions { + display: flex; + align-items: center; + gap: 32px; + + .icons { + display: flex; + align-items: center; + justify-content: center; + + i { + display: flex; + align-items: center; + justify-content: center; + + &.search-icon { + width: 40px; + height: 36px; + + svg, + svg * { + width: 18px; + height: 18px; + fill: $COLOR--e-300; + } + } + } + } +} diff --git a/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.jsx b/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.jsx index 8a44b6db40..0456eb8c6f 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.jsx +++ b/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.jsx @@ -21,11 +21,11 @@ import { useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { activeProjectKeySelector } from 'controllers/user'; import { AbsRelTime } from 'components/main/absRelTime'; -import { MeatballMenuIcon, Popover, Table } from '@reportportal/ui-kit'; +import { MeatballMenuIcon, Popover } from '@reportportal/ui-kit'; import { UserAvatar } from 'pages/inside/common/userAvatar'; import { urlOrganizationAndProjectSelector, userRolesSelector } from 'controllers/pages'; import { SORTING_ASC, withSortingURL } from 'controllers/sorting'; -import { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_SORT_COLUMN } from 'controllers/members/constants'; +import { DEFAULT_SORT_COLUMN } from 'controllers/members/constants'; import { fetchMembersAction, membersPaginationSelector } from 'controllers/members'; import { canSeeEmailMembers, getRoleTitle } from 'common/utils/permissions'; import { canSeeRowActionMenu } from 'common/utils/permissions/permissions'; @@ -35,9 +35,9 @@ import { PAGE_KEY, withPagination, } from 'controllers/pagination'; -import { PaginationWrapper } from 'components/main/paginationWrapper'; -import { messages } from './messages'; +import { messages } from '../../common/membersPage/messages'; import styles from './projectTeamListTable.scss'; +import { MembersListTable } from '../../common/membersPage/membersListTable'; const cx = classNames.bind(styles); @@ -158,28 +158,21 @@ const ProjectTeamListTableWrapped = ({ }; return ( - 0} + rowActionMenu={canSeeRowActionMenu(userRoles) ? rowActionMenu : null} + sortingDirection={sortingDirection} pageSize={pageSize} activePage={activePage} - totalItems={itemCount} - totalPages={pageCount} - pageSizeOptions={DEFAULT_PAGE_SIZE_OPTIONS} - changePage={onChangePage} - changePageSize={onChangePageSize} - > -
- + itemCount={itemCount} + pageCount={pageCount} + onChangePage={onChangePage} + onChangePageSize={onChangePageSize} + /> ); }; diff --git a/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.scss b/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.scss index 546daaa234..efdef4149e 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.scss +++ b/app/src/pages/organization/projectTeamPage/projectTeamListTable/projectTeamListTable.scss @@ -14,13 +14,6 @@ * limitations under the License. */ -.project-team-list-table { - margin-top: 24px; - padding: 0 32px; - max-width: 1264px; - box-sizing: border-box; -} - .date { font-family: inherit; color: inherit; @@ -65,10 +58,3 @@ border-radius: 50%; } } - -.loader { - display: flex; - justify-content: center; - align-items: center; - height: 100%; -} diff --git a/app/src/pages/organization/projectTeamPage/projectTeamPage.jsx b/app/src/pages/organization/projectTeamPage/projectTeamPage.jsx index 51c0ace9a0..3981c2439e 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamPage.jsx +++ b/app/src/pages/organization/projectTeamPage/projectTeamPage.jsx @@ -19,22 +19,17 @@ import { userRolesSelector } from 'controllers/pages'; import { canInviteInternalUser } from 'common/utils/permissions'; import classNames from 'classnames/bind'; import { loadingSelector, membersSelector, fetchMembersAction } from 'controllers/members'; -import { useIntl } from 'react-intl'; -import { BubblesLoader } from '@reportportal/ui-kit'; import { ScrollWrapper } from 'components/main/scrollWrapper'; import { showModalAction } from 'controllers/modal'; -import { EmptyPageState } from 'pages/common/emptyPageState'; +import { EmptyMembersPageState } from '../common/membersPage/emptyMembersPageState'; import { ProjectTeamPageHeader } from './projectTeamPageHeader'; import { ProjectTeamListTable } from './projectTeamListTable'; -import EmptyIcon from './img/empty-members-icon-inline.svg'; -import { messages } from './messages'; import styles from './projectTeamPage.scss'; const cx = classNames.bind(styles); export const ProjectTeamPage = () => { const dispatch = useDispatch(); - const { formatMessage } = useIntl(); const userRoles = useSelector(userRolesSelector); const hasPermission = canInviteInternalUser(userRoles); const members = useSelector(membersSelector); @@ -54,32 +49,23 @@ export const ProjectTeamPage = () => { ); }; - const getEmptyPageState = () => - isMembersLoading ? ( -
- -
- ) : ( - - ); - return (
- {isEmptyMembers ? getEmptyPageState() : } + {isEmptyMembers ? ( + + ) : ( + + )}
); diff --git a/app/src/pages/organization/projectTeamPage/projectTeamPage.scss b/app/src/pages/organization/projectTeamPage/projectTeamPage.scss index 673e460dc7..b7aebd6eb5 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamPage.scss +++ b/app/src/pages/organization/projectTeamPage/projectTeamPage.scss @@ -19,10 +19,3 @@ flex-direction: column; min-height: 100%; } - -.loader { - display: flex; - justify-content: center; - align-items: center; - height: 100%; -} diff --git a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx index d5c30268f4..0b4819f384 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx +++ b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx @@ -18,49 +18,49 @@ import React from 'react'; import PropTypes from 'prop-types'; import Parser from 'html-react-parser'; import classNames from 'classnames/bind'; -import { Button } from '@reportportal/ui-kit'; -import searchIcon from 'common/img/newIcons/search-outline-inline.svg'; +import { Button, SearchIcon } from '@reportportal/ui-kit'; import filterIcon from 'common/img/newIcons/filters-outline-inline.svg'; import { useIntl } from 'react-intl'; -import { messages } from '../messages'; +import { messages } from '../../common/membersPage/membersPageHeader/messages'; import styles from './projectTeamPageHeader.scss'; +import { MembersPageHeader } from '../../common/membersPage/membersPageHeader'; const cx = classNames.bind(styles); -export const ProjectTeamPageHeader = ({ hasPermission, isNotEmpty, showInviteUserModal }) => { +export const ProjectTeamPageHeader = ({ hasPermission, isNotEmpty, onInvite }) => { const { formatMessage } = useIntl(); return ( -
-
- {formatMessage(messages.title)} -
- {isNotEmpty && ( - <> -
- {Parser(searchIcon)} - {Parser(filterIcon)} -
- {hasPermission && ( - - )} - - )} -
+ +
+ {isNotEmpty && ( + <> +
+ + + + {Parser(filterIcon)} +
+ {hasPermission && ( + + )} + + )}
-
+ ); }; ProjectTeamPageHeader.propTypes = { hasPermission: PropTypes.bool, isNotEmpty: PropTypes.bool, - showInviteUserModal: PropTypes.func.isRequired, + onInvite: PropTypes.func, }; ProjectTeamPageHeader.defaultProps = { hasPermission: false, isNotEmpty: false, + onInvite: () => {}, }; diff --git a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.scss b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.scss index 014f95d9c1..fa54057d6e 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.scss +++ b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.scss @@ -14,33 +14,6 @@ * limitations under the License. */ -.project-team-page-header-container { - padding: 16px 32px; - border-bottom: 1px solid $COLOR--e-100; - background: $COLOR--bg-000; - box-sizing: border-box; - position: sticky; - top: 0; - z-index: 2; -} - -.header { - display: flex; - min-height: 31px; - justify-content: space-between; -} - -.title { - font-family: $FONT-REGULAR; - font-size: 20px; - line-height: 31px; - color: $COLOR--almost-black; - text-transform: capitalize; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - .actions { display: flex; align-items: center; @@ -59,16 +32,23 @@ &.search-icon { width: 40px; height: 36px; + + svg, + svg * { + width: 18px; + height: 18px; + fill: $COLOR--e-300; + } } &.filters-icon { width: 40px; height: 36px; - } - svg { - width: 16px; - height: 16px; + svg { + width: 16px; + height: 16px; + } } } } diff --git a/app/src/routes/constants.js b/app/src/routes/constants.js index 33f5e3ffa0..868611a850 100644 --- a/app/src/routes/constants.js +++ b/app/src/routes/constants.js @@ -54,6 +54,7 @@ import { USER_PROFILE_SUB_PAGE, USER_PROFILE_SUB_PAGE_ORGANIZATION_LEVEL, USER_PROFILE_SUB_PAGE_PROJECT_LEVEL, + ORGANIZATION_USERS_PAGE, } from 'controllers/pages'; import { AdminUiExtensionPage } from 'pages/instance/adminUiExtensionPage'; import { AccountRemovedPage } from 'pages/outside/accountRemovedPage'; @@ -63,6 +64,7 @@ import { ProjectTeamPage } from 'pages/organization/projectTeamPage'; import { ProjectLayout } from 'layouts/projectLayout'; import { OrganizationLayout } from 'layouts/organizationLayout'; import { InstanceLayout } from 'layouts/instanceLayout'; +import { OrganizationUsersPage } from 'pages/organization/organizationUsersPage'; import { OrganizationsPage } from 'pages/instance/organizationsPage'; export const ANONYMOUS_ACCESS = 'anonymous'; @@ -89,6 +91,11 @@ export const pageRendering = { [USER_PROFILE_PAGE_PROJECT_LEVEL]: { component: ProfilePage, layout: ProjectLayout }, [USER_PROFILE_SUB_PAGE_PROJECT_LEVEL]: { component: ProfilePage, layout: ProjectLayout }, API_PAGE: { component: ApiPage, layout: ProjectLayout }, + [ORGANIZATION_USERS_PAGE]: { + component: OrganizationUsersPage, + layout: OrganizationLayout, + rawContent: true, + }, ORGANIZATIONS_PAGE: { component: OrganizationsPage, layout: InstanceLayout, diff --git a/app/src/routes/routesMap.js b/app/src/routes/routesMap.js index 7d882453bc..2394eae63b 100644 --- a/app/src/routes/routesMap.js +++ b/app/src/routes/routesMap.js @@ -56,7 +56,7 @@ import { PROJECT_PLUGIN_PAGE, userAssignedSelector, ORGANIZATION_PROJECTS_PAGE, - ORGANIZATION_MEMBERS_PAGE, + ORGANIZATION_USERS_PAGE, ORGANIZATION_SETTINGS_PAGE, USER_PROFILE_PAGE, USER_PROFILE_PAGE_ORGANIZATION_LEVEL, @@ -99,7 +99,8 @@ import { fetchOrganizationsAction } from 'controllers/instance/organizations'; import { fetchOrganizationBySlugAction, prepareActiveOrganizationProjectsAction, -} from 'controllers/organization'; +} from 'controllers/organization/actionCreators'; +import { prepareActiveOrganizationUsersAction } from 'controllers/organization/users'; import { pageRendering, ANONYMOUS_ACCESS, ADMIN_ACCESS } from './constants'; const redirectRoute = (path, createNewAction, onRedirect = () => {}) => ({ @@ -189,8 +190,14 @@ const routesMap = { }, }, - [ORGANIZATION_MEMBERS_PAGE]: { - path: '/organizations/:organizationSlug/members', + [ORGANIZATION_USERS_PAGE]: { + path: '/organizations/:organizationSlug/users', + thunk: (dispatch, getState) => { + const { + location: { payload }, + } = getState(); + dispatch(prepareActiveOrganizationUsersAction(payload)); + }, }, [ORGANIZATION_SETTINGS_PAGE]: { diff --git a/app/src/store/rootSaga.js b/app/src/store/rootSaga.js index 70e84c979e..42644c59b8 100644 --- a/app/src/store/rootSaga.js +++ b/app/src/store/rootSaga.js @@ -36,6 +36,7 @@ import { pageSagas } from 'controllers/pages'; import { pluginSagas } from 'controllers/plugins'; import { uniqueErrorsSagas } from 'controllers/uniqueErrors'; import { organizationsSagas } from 'controllers/instance/organizations'; +import { organizationSagas } from 'controllers/organization'; const sagas = [ notificationSagas, @@ -54,6 +55,7 @@ const sagas = [ instanceSagas, userSagas, organizationsSagas, + organizationSagas, projectSagas, initialDataSagas, pageSagas,