diff --git a/app/localization/translated/be.json b/app/localization/translated/be.json index c63db82272..02eea9af3d 100644 --- a/app/localization/translated/be.json +++ b/app/localization/translated/be.json @@ -1504,6 +1504,15 @@ "NestedGridRow.loadLabel": "Загрузіць наступныя 300", "NestedGridRow.loadPreviousLabel": "Загрузіць папярэднія 300", "NewsBlock.twitterTitle": "Азнаёміцца з нашымі апошнімі твітамі", + "noAssignedEmptyPage.description": "Для пачатку вам неабходна зарэгістравацца ў арганізацыі. Звяжыцеся з членамі існуючай арганізацыі, да якой вы хочаце далучыцца, і атрымаеце запрашэнні. Для атрымання дадатковай інфармацыі пра наладу арганізацыі звярніцеся да свайго адміністратара.", + "noAssignedEmptyPage.descriptionSaaS": "Каб пачаць, вам трэба ўступіць у якую-небудзь арганізацыю. Вось як:", + "noAssignedEmptyPage.contactUs": "Звяжыцеся з намі, каб стварыць сваю ўласную арганізацыю.", + "noAssignedEmptyPage.createOwnOrganization": "Стварыце сваю ўласную арганізацыю", + "noAssignedEmptyPage.existingOrganization": "Далучыцца да існуючай арганізацыі", + "noAssignedEmptyPage.invitations": "Звяжыцеся з членамі існуючай арганізацыі, да якой вы хочаце далучыцца, і атрымаеце запрашэнні ад іх.", + "noAssignedEmptyPage.or": "АБО", + "noAssignedEmptyPage.reviewPricing": "Азнаёмцеся з цэнамі", + "noAssignedEmptyPage.title": "Вітаю", "NoCasesBlock.noItemsMessage": "Няма правилаў паведамлення па электроннай пошце", "NoCasesBlock.notificationsInfo": "Пасля завяршэння запуску сістэма паведаміць выбраным людзям па электроннай пошце", "NoDataAvailable.noDataMessage": "Няма даступных дадзеных..", diff --git a/app/localization/translated/es.json b/app/localization/translated/es.json index d34581bed6..b5728231eb 100644 --- a/app/localization/translated/es.json +++ b/app/localization/translated/es.json @@ -1503,6 +1503,15 @@ "NestedGridRow.loadLabel": "Cargar siguientes 300", "NestedGridRow.loadPreviousLabel": "Cargar anteriores 300", "NewsBlock.twitterTitle": "Consulte nuestros últimos tweets", + "noAssignedEmptyPage.description": "To get started, you need to join an organization. Contact and receive invitations from members of an existing organization you want to join. For advanced organization configuration reach out to your administrator.", + "noAssignedEmptyPage.descriptionSaaS": "To get started, you need to join an organization. Here's how:", + "noAssignedEmptyPage.contactUs": "Contact us to set up your own organization.", + "noAssignedEmptyPage.createOwnOrganization": "Create your own organization", + "noAssignedEmptyPage.existingOrganization": "Join an existing organization", + "noAssignedEmptyPage.invitations": "Contact and receive invitations from members of an existing organization you want to join.", + "noAssignedEmptyPage.or": "OR", + "noAssignedEmptyPage.reviewPricing": "Review Pricing", + "noAssignedEmptyPage.title": "Welcome", "NoCasesBlock.noItemsMessage": "No hay reglas de notificación por correo electrónico", "NoCasesBlock.notificationsInfo": "Después de la finalización de la ejecución, el sistema notificará por correo electrónico a las personas seleccionadas", "NoDataAvailable.noDataMessage": "No hay datos disponibles.", diff --git a/app/localization/translated/ru.json b/app/localization/translated/ru.json index a09d9f9939..0f5cb6883d 100644 --- a/app/localization/translated/ru.json +++ b/app/localization/translated/ru.json @@ -1500,6 +1500,15 @@ "NestedGridRow.loadLabel": "Загрузить следующие 300", "NestedGridRow.loadPreviousLabel": "Загрузить предыдущие 300", "NewsBlock.twitterTitle": "Ознакомьтесь с нашими последними твитами", + "noAssignedEmptyPage.description": "Для начала вам необходимо зарегистрироваться в организации. Свяжитесь с членами существующей организации, к которой вы хотите присоединиться, и получите приглашения. Для получения дополнительной информации о настройке организации обратитесь к своему администратору.", + "noAssignedEmptyPage.descriptionSaaS": "Чтобы начать, вам нужно вступить в какую-либо организацию. Вот как:", + "noAssignedEmptyPage.contactUs": "Свяжитесь с нами, чтобы создать свою собственную организацию.", + "noAssignedEmptyPage.createOwnOrganization": "Создайте свою собственную организацию", + "noAssignedEmptyPage.existingOrganization": "Присоединиться к существующей организации", + "noAssignedEmptyPage.invitations": "Свяжитесь с членами существующей организации, к которой вы хотите присоединиться, и получите приглашения от них.", + "noAssignedEmptyPage.or": "ИЛИ", + "noAssignedEmptyPage.reviewPricing": "Ознакомьтесь с ценами", + "noAssignedEmptyPage.title": "Добро пожаловать", "NoCasesBlock.noItemsMessage": "Нет правил уведомления по электронной почте", "NoCasesBlock.notificationsInfo": "После завершения запуска система уведомит выбранных людей по электронной почте", "NoDataAvailable.noDataMessage": "Нет доступных данных.", diff --git a/app/localization/translated/uk.json b/app/localization/translated/uk.json index aecfa94f2f..67bce27740 100644 --- a/app/localization/translated/uk.json +++ b/app/localization/translated/uk.json @@ -1502,6 +1502,15 @@ "NestedGridRow.loadLabel": "Завантажити наступні 300", "NestedGridRow.loadPreviousLabel": "Завантажити попередні 300", "NewsBlock.twitterTitle": "Ознайомтеся з нашими останніми твітами", + "noAssignedEmptyPage.description": "To get started, you need to join an organization. Contact and receive invitations from members of an existing organization you want to join. For advanced organization configuration reach out to your administrator.", + "noAssignedEmptyPage.descriptionSaaS": "Щоб почати, вам потрібно вступити в якусь організацію. Ось як:", + "noAssignedEmptyPage.contactUs": "Зв'яжіться з нами, щоб створити власну організацію.", + "noAssignedEmptyPage.createOwnOrganization": "Створіть власну організацію", + "noAssignedEmptyPage.existingOrganization": "Приєднатися до існуючої організації", + "noAssignedEmptyPage.invitations": "Зв'яжіться з членами існуючої організації, до якої Ви хочете приєднатися, і отримайте запрошення від них.", + "noAssignedEmptyPage.or": "АБО", + "noAssignedEmptyPage.reviewPricing": "Ознайомтеся з цінами", + "noAssignedEmptyPage.title": "Вітати", "NoCasesBlock.noItemsMessage": "Правил повідомлення по електронній пошті Немає", "NoCasesBlock.notificationsInfo": "Після завершення запуску система сповістить обраних людей електронною поштою", "NoDataAvailable.noDataMessage": "Доступних даних Немає.", diff --git a/app/localization/translated/zh.json b/app/localization/translated/zh.json index c6ae298e59..ab093ce385 100644 --- a/app/localization/translated/zh.json +++ b/app/localization/translated/zh.json @@ -1502,6 +1502,15 @@ "NestedGridRow.loadLabel": "再加载10个", "NestedGridRow.loadPreviousLabel": "加载前300", "NewsBlock.twitterTitle": "了解我们最新的推文", + "noAssignedEmptyPage.description": "To get started, you need to join an organization. Contact and receive invitations from members of an existing organization you want to join. For advanced organization configuration reach out to your administrator.", + "noAssignedEmptyPage.descriptionSaaS": "To get started, you need to join an organization. Here's how:", + "noAssignedEmptyPage.contactUs": "Contact us to set up your own organization.", + "noAssignedEmptyPage.createOwnOrganization": "Create your own organization", + "noAssignedEmptyPage.existingOrganization": "Join an existing organization", + "noAssignedEmptyPage.invitations": "Contact and receive invitations from members of an existing organization you want to join.", + "noAssignedEmptyPage.or": "OR", + "noAssignedEmptyPage.reviewPricing": "Review Pricing", + "noAssignedEmptyPage.title": "Welcome", "NoCasesBlock.noItemsMessage": "无电子邮件通知规则", "NoCasesBlock.notificationsInfo": "测试任务完成后系统将通过电子邮件通知选定的人", "NoDataAvailable.noDataMessage": "无可用数据。", diff --git a/app/src/controllers/initialData/sagas.js b/app/src/controllers/initialData/sagas.js index f315777084..e427d93b17 100644 --- a/app/src/controllers/initialData/sagas.js +++ b/app/src/controllers/initialData/sagas.js @@ -41,12 +41,12 @@ function* fetchInitialData() { yield put(fetchUserAction()); const userResult = yield take([FETCH_USER_SUCCESS, FETCH_USER_ERROR]); if (!userResult.error) { + yield put(authSuccessAction()); const { payload: activeProjectKey } = yield take(SET_ACTIVE_PROJECT_KEY); yield put(fetchProjectAction(activeProjectKey)); yield take(FETCH_PROJECT_SUCCESS); yield put(fetchPluginsAction()); yield put(fetchGlobalIntegrationsAction()); - yield put(authSuccessAction()); } else { yield put(resetTokenAction()); yield put(fetchPublicPluginsAction()); diff --git a/app/src/controllers/project/sagas.js b/app/src/controllers/project/sagas.js index 2f7ac68c90..33103c4af2 100644 --- a/app/src/controllers/project/sagas.js +++ b/app/src/controllers/project/sagas.js @@ -416,14 +416,17 @@ function* watchUpdatePAState() { function* fetchProject({ payload: { projectKey, fetchInfoOnly } }) { try { - const project = yield call(fetch, URLS.projectByName(projectKey)); + let project = null; + if (projectKey) { + project = yield call(fetch, URLS.projectByName(projectKey)); + yield put(setProjectIntegrationsAction(project.integrations)); + } yield put(fetchProjectSuccessAction(project)); - yield put(setProjectIntegrationsAction(project.integrations)); const userRoles = yield select(userRolesSelector); const hasFilterPermissions = canWorkWithFilters(userRoles); - if (!fetchInfoOnly && hasFilterPermissions) { + if (!fetchInfoOnly && hasFilterPermissions && projectKey) { yield put(fetchProjectPreferencesAction(project.projectKey)); } } catch (error) { diff --git a/app/src/controllers/user/sagas.js b/app/src/controllers/user/sagas.js index 5eb052b208..1d9950fec5 100644 --- a/app/src/controllers/user/sagas.js +++ b/app/src/controllers/user/sagas.js @@ -15,12 +15,17 @@ */ import { takeLatest, takeEvery, call, all, put, select } from 'redux-saga/effects'; +import { redirect } from 'redux-first-router'; import { fetch } from 'common/utils/fetch'; import { URLS } from 'common/urls'; import { showNotification, NOTIFICATION_TYPES } from 'controllers/notification'; import { PROJECT_MANAGER } from 'common/constants/projectRoles'; import { getStorageItem, setStorageItem } from 'common/utils/storageUtils'; -import { userAssignedSelector, urlOrganizationAndProjectSelector } from 'controllers/pages'; +import { + userAssignedSelector, + urlOrganizationAndProjectSelector, + ORGANIZATIONS_PAGE, +} from 'controllers/pages'; import { getLogTimeFormatFromStorage } from 'controllers/log/storageUtils'; import { setActiveOrganizationAction } from 'controllers/organization/actionCreators'; import { findAssignedProjectByOrganization } from 'common/utils'; @@ -134,71 +139,81 @@ function* fetchUserWorker() { } const urlOrganizationAndProject = yield select(urlOrganizationAndProjectSelector); const { userId, assignedOrganizations, assignedProjects } = user; - const userSettings = getStorageItem(`${userId}_settings`) || {}; - const targetActiveProject = urlOrganizationAndProject || userSettings?.activeProject; - const { organizationSlug: targetOrganizationSlug, projectSlug: targetProjectSlug } = - targetActiveProject || {}; - - const { assignmentNotRequired, isAssignedToTargetProject } = yield select( - userAssignedSelector(targetProjectSlug, targetOrganizationSlug), - ); - - const defaultProject = Object.keys(assignedProjects)[0]; - const { - projectSlug: defaultProjectSlug, - projectKey: defaultProjectKey, - organizationId, - } = assignedProjects[defaultProject]; - const defaultOrganization = Object.keys(assignedOrganizations).find( - (key) => assignedOrganizations[key].organizationId === organizationId, - ); - const { organizationSlug: defaultOrganizationSlug } = defaultOrganization - ? assignedOrganizations[defaultOrganization] - : Object.keys(assignedOrganizations)[0]; - - let projectKey; - let activeOrganization; - try { - const activeOrganizationResponse = yield call( - fetch, - URLS.organizationList({ slug: targetOrganizationSlug }), + const format = getLogTimeFormatFromStorage(userId); + yield put(setLogTimeFormatAction(format)); + + if (Object.keys(assignedOrganizations).length === 0) { + yield put(setActiveProjectKeyAction(null)); + yield put( + redirect({ + type: ORGANIZATIONS_PAGE, + }), + ); + } else { + const userSettings = getStorageItem(`${userId}_settings`) || {}; + const targetActiveProject = urlOrganizationAndProject || userSettings?.activeProject; + const { organizationSlug: targetOrganizationSlug, projectSlug: targetProjectSlug } = + targetActiveProject || {}; + + const { assignmentNotRequired, isAssignedToTargetProject } = yield select( + userAssignedSelector(targetProjectSlug, targetOrganizationSlug), + ); + + const defaultProject = Object.keys(assignedProjects)[0]; + const { + projectSlug: defaultProjectSlug, + projectKey: defaultProjectKey, + organizationId, + } = assignedProjects[defaultProject]; + const defaultOrganization = Object.keys(assignedOrganizations).find( + (key) => assignedOrganizations[key].organizationId === organizationId, ); + const { organizationSlug: defaultOrganizationSlug } = defaultOrganization + ? assignedOrganizations[defaultOrganization] + : Object.keys(assignedOrganizations)[0]; - activeOrganization = activeOrganizationResponse?.items?.[0]; - } catch (e) {} // eslint-disable-line no-empty + let projectKey; + let activeOrganization; - if (!isAssignedToTargetProject && assignmentNotRequired) { try { - // TODO: Fetch project by slug - const organizationProjects = yield call( + const activeOrganizationResponse = yield call( fetch, - URLS.organizationProjects(activeOrganization?.id), + URLS.organizationList({ slug: targetOrganizationSlug }), ); - projectKey = organizationProjects?.items?.find(({ slug }) => slug === targetProjectSlug)?.key; + + activeOrganization = activeOrganizationResponse?.items?.[0]; } catch (e) {} // eslint-disable-line no-empty - } - const activeProject = - isAssignedToTargetProject || projectKey - ? targetActiveProject - : { organizationSlug: defaultOrganizationSlug, projectSlug: defaultProjectSlug }; + if (!isAssignedToTargetProject && assignmentNotRequired) { + try { + const currentProject = yield call( + fetch, + URLS.organizationProjects(activeOrganization?.id, { slug: activeOrganization?.slug }), + ); + projectKey = currentProject?.items?.[0]?.key; + } catch (e) {} // eslint-disable-line no-empty + } - if (!projectKey) { - const assignedProject = findAssignedProjectByOrganization( - assignedProjects, - assignedOrganizations[targetOrganizationSlug]?.organizationId, - targetProjectSlug, - ); + const activeProject = + isAssignedToTargetProject || projectKey + ? targetActiveProject + : { organizationSlug: defaultOrganizationSlug, projectSlug: defaultProjectSlug }; - projectKey = isAssignedToTargetProject ? assignedProject.projectKey : defaultProjectKey; - } + if (!projectKey) { + const assignedProject = findAssignedProjectByOrganization( + assignedProjects, + assignedOrganizations[targetOrganizationSlug]?.organizationId, + targetProjectSlug, + ); - yield put(setActiveProjectAction(activeProject)); - yield put(setActiveProjectKeyAction(projectKey)); - yield put(setActiveOrganizationAction(activeOrganization)); - const format = getLogTimeFormatFromStorage(user.userId); - yield put(setLogTimeFormatAction(format)); + projectKey = isAssignedToTargetProject ? assignedProject.projectKey : defaultProjectKey; + } + + yield put(setActiveProjectAction(activeProject)); + yield put(setActiveProjectKeyAction(projectKey)); + yield put(setActiveOrganizationAction(activeOrganization)); + } } function* saveActiveProjectWorker({ payload: activeProject }) { diff --git a/app/src/controllers/user/selectors.js b/app/src/controllers/user/selectors.js index b23fd4ae8e..05fd2ba903 100644 --- a/app/src/controllers/user/selectors.js +++ b/app/src/controllers/user/selectors.js @@ -27,6 +27,7 @@ export const idSelector = (state) => userInfoSelector(state).id; export const userIdSelector = (state) => userInfoSelector(state).userId; export const userEmailSelector = (state) => userInfoSelector(state).email || ''; export const photoIdSelector = (state) => userInfoSelector(state).photoId; +export const fullNameSelector = (state) => userInfoSelector(state).fullName; export const settingsSelector = (state) => userSelector(state).settings || {}; export const startTimeFormatSelector = (state) => settingsSelector(state).startTimeFormat || START_TIME_FORMAT_RELATIVE; diff --git a/app/src/layouts/instanceLayout/instanceSidebar/instanceSidebar.jsx b/app/src/layouts/instanceLayout/instanceSidebar/instanceSidebar.jsx index e6a01821df..ffbd6a09c9 100644 --- a/app/src/layouts/instanceLayout/instanceSidebar/instanceSidebar.jsx +++ b/app/src/layouts/instanceLayout/instanceSidebar/instanceSidebar.jsx @@ -40,6 +40,8 @@ import OrganizationsIcon from 'common/img/sidebar/organizations-icon-inline.svg' import UsersIcon from 'common/img/sidebar/members-icon-inline.svg'; import SettingsIcon from 'common/img/sidebar/settings-icon-inline.svg'; import PluginsIcon from 'common/img/sidebar/plugins-icon-inline.svg'; +import { ADMINISTRATOR } from 'common/constants/accountRoles'; +import { assignedOrganizationsSelector } from 'controllers/user'; import { OrganizationsControlWithPopover } from '../../organizationsControl'; import { messages } from '../../messages'; @@ -51,6 +53,10 @@ export const InstanceSidebar = ({ onClickNavBtn }) => { const userRoles = useSelector(userRolesSelector); const sidebarExtensions = useSelector(uiExtensionSidebarComponentsSelector); const adminPageExtensions = useSelector(uiExtensionAdminPagesSelector); + const assignedOrganizations = useSelector(assignedOrganizationsSelector); + const noAssignedOrganizations = + Object.keys(assignedOrganizations).length === 0 && userRoles.userRole !== ADMINISTRATOR; + const [isOpenOrganizationPopover, setIsOpenOrganizationPopover] = useState(false); const onClickButton = (eventInfo) => { @@ -149,7 +155,7 @@ export const InstanceSidebar = ({ onClickNavBtn }) => { return ( {} : createMainBlock} items={getSidebarItems()} isOpenOrganizationPopover={isOpenOrganizationPopover} linkToUserProfilePage={linkToUserProfilePage} diff --git a/app/src/layouts/organizationsControl/organizationsPopover/organizationsPopover.jsx b/app/src/layouts/organizationsControl/organizationsPopover/organizationsPopover.jsx index 576c3cf1fd..d60441ab90 100644 --- a/app/src/layouts/organizationsControl/organizationsPopover/organizationsPopover.jsx +++ b/app/src/layouts/organizationsControl/organizationsPopover/organizationsPopover.jsx @@ -106,19 +106,21 @@ export const OrganizationsPopover = ({ closePopover, closeSidebar }) => { return (
- - } - placeholder={formatMessage(COMMON_LOCALE_KEYS.SEARCH)} - className={cx('field-text')} - defaultWidth={false} - value={valueSearch} - onChange={handleChange} - onClear={handleClear} - maxLength={256} - clearable - /> - + {filteredProjects.length > 0 && ( + + } + placeholder={formatMessage(COMMON_LOCALE_KEYS.SEARCH)} + className={cx('field-text')} + defaultWidth={false} + value={valueSearch} + onChange={handleChange} + onClear={handleClear} + maxLength={256} + clearable + /> + + )}
{availableProjects.length > 0 && ( <> diff --git a/app/src/pages/instance/organizationsPage/img/footer.png b/app/src/pages/instance/organizationsPage/img/footer.png new file mode 100644 index 0000000000..b70e1d121e Binary files /dev/null and b/app/src/pages/instance/organizationsPage/img/footer.png differ diff --git a/app/src/pages/instance/organizationsPage/img/rocket-inline.svg b/app/src/pages/instance/organizationsPage/img/rocket-inline.svg new file mode 100644 index 0000000000..a1dedaf11b --- /dev/null +++ b/app/src/pages/instance/organizationsPage/img/rocket-inline.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/index.js b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/index.js new file mode 100644 index 0000000000..4a2beb828c --- /dev/null +++ b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/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 { NoAssignedEmptyPage } from './noAssignedEmptyPage'; diff --git a/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/messages.js b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/messages.js new file mode 100644 index 0000000000..5f100577bc --- /dev/null +++ b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/messages.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 { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + title: { + id: 'NoAssignedEmptyPage.title', + defaultMessage: 'Welcome', + }, + description: { + id: 'NoAssignedEmptyPage.description', + defaultMessage: + 'To get started, you need to join an organization. Contact and receive invitations from members of an existing organization you want to join. For advanced organization configuration reach out to your administrator.', + }, + descriptionSaaS: { + id: 'NoAssignedEmptyPage.descriptionSaaS', + defaultMessage: `To get started, you need to join an organization. Here's how:`, + }, + existingOrganization: { + id: 'NoAssignedEmptyPage.existingOrganization', + defaultMessage: 'Join an existing organization', + }, + invitations: { + id: 'NoAssignedEmptyPage.invitations', + defaultMessage: + 'Contact and receive invitations from members of an existing organization you want to join.', + }, + createOwnOrganization: { + id: 'NoAssignedEmptyPage.createOwnOrganization', + defaultMessage: 'Create your own organization', + }, + contactUs: { + id: 'NoAssignedEmptyPage.contactUs', + defaultMessage: 'Contact us to set up your own organization.', + }, + reviewPricing: { + id: 'NoAssignedEmptyPage.reviewPricing', + defaultMessage: 'Review Pricing', + }, + or: { + id: 'NoAssignedEmptyPage.or', + defaultMessage: 'OR', + }, +}); diff --git a/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.jsx b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.jsx new file mode 100644 index 0000000000..56366e2b7d --- /dev/null +++ b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.jsx @@ -0,0 +1,80 @@ +/* + * 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 classNames from 'classnames/bind'; +import Parser from 'html-react-parser'; +import { fullNameSelector } from 'controllers/user/selectors'; +import OpenIcon from 'common/img/open-in-rounded-inline.svg'; +import { SAAS } from 'controllers/appInfo/constants'; +import { useIntl } from 'react-intl'; +import { instanceTypeSelector } from 'controllers/appInfo/selectors'; +import RocketIcon from '../img/rocket-inline.svg'; +import FooterImg from '../img/footer.png'; +import { messages } from './messages'; +import styles from './noAssignedEmptyPage.scss'; + +const cx = classNames.bind(styles); + +export const NoAssignedEmptyPage = () => { + const { formatMessage } = useIntl(); + const fullName = useSelector(fullNameSelector); + const instanceType = useSelector(instanceTypeSelector); + const isSaaS = instanceType !== SAAS; + const description = isSaaS + ? formatMessage(messages.descriptionSaaS) + : formatMessage(messages.description); + + const getSaasRender = () => { + return ( +
+
+

{formatMessage(messages.existingOrganization)}

+

{formatMessage(messages.invitations)}

+
+
+
{formatMessage(messages.or)}
+
+
+
+

{formatMessage(messages.createOwnOrganization)}

+

{formatMessage(messages.contactUs)}

+ +
{formatMessage(messages.reviewPricing)}
+
{Parser(OpenIcon)}
+
+
+
+ ); + }; + + return ( +
+
+
{Parser(RocketIcon)}
+
{`${formatMessage(messages.title)}, ${fullName}!`}
+
{description}
+ {isSaaS && getSaasRender()} +
+ footer +
+ ); +}; diff --git a/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.scss b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.scss new file mode 100644 index 0000000000..9aa916285b --- /dev/null +++ b/app/src/pages/instance/organizationsPage/noAssignedEmptyPage/noAssignedEmptyPage.scss @@ -0,0 +1,137 @@ +/* + * 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. + */ + +.rocket-icon { + width: 113px; + height: 190px; + margin-bottom: -45px; +} + +.footer-icon { + position: absolute; + bottom: 0; + width: 100vw; + left: -48px; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(100vh - 200px); +} + +.title { + font-size: 32px; + line-height: 42px; + color: $COLOR--e-400; +} + +.description { + font-family: $FONT-ROBOTO-REGULAR; + font-size: 13px; + line-height: 20px; + color: $COLOR--almost-black; + width: 530px; + text-align: center; + margin-top: 16px; +} + +.saas-container { + margin-top: 24px; + max-width: 820px; + background-color: $COLOR--bg-000; + display: flex; + border-radius: 6px; + box-shadow: 0px 1px 3px 0px $COLOR--item-shadow; +} + +.block { + width: 300px; + text-align: center; + padding: 30px; +} + +.block-title { + font-family: $FONT-ROBOTO-MEDIUM; + font-size: 15px; + line-height: 24px; +} + +.block-description { + font-family: $FONT-ROBOTO-REGULAR; + font-size: 13px; + line-height: 20px; + margin-top: 8px; +} + +.separator-container { + position: relative; + display: flex; + align-items: center; +} + +.separator { + width: 2px; + height: 140px; + background-color: $COLOR--bg-200; + position: absolute; + top: 0; + left: 14px; +} + +.text { + font-family: $FONT-ROBOTO-BOLD; + font-size: 11px; + line-height: 20px; + width: 30px; + height: 30px; + border-radius: 50%; + color: $COLOR--e-400; + background-color: $COLOR--darkmode-gray-50; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.link-container { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + margin-top: 8px; + text-decoration: none; +} + +.link { + font-family: $FONT-ROBOTO-MEDIUM; + font-size: 13px; + line-height: 20px; + color: $COLOR--topaz-2; +} + +.open-icon { + svg { + width: 13px; + height: 13px; + + path { + fill: $COLOR--e-300; + } + } +} diff --git a/app/src/pages/instance/organizationsPage/organizationsPage.jsx b/app/src/pages/instance/organizationsPage/organizationsPage.jsx index 394ce1c0db..f246c5e6d0 100644 --- a/app/src/pages/instance/organizationsPage/organizationsPage.jsx +++ b/app/src/pages/instance/organizationsPage/organizationsPage.jsx @@ -30,11 +30,12 @@ import { import { COMMON_LOCALE_KEYS } from 'common/constants/localization'; import NoResultsIcon from 'common/img/newIcons/no-results-icon-inline.svg'; import { getStorageItem, updateStorageItem } from 'common/utils/storageUtils'; -import { userIdSelector } from 'controllers/user'; +import { assignedOrganizationsSelector, userIdSelector } from 'controllers/user'; import EmptyIcon from './img/empty-organizations-inline.svg'; import { OrganizationsPageHeader } from './organizationsPageHeader'; import { OrganizationsPanelView } from './organizationsPanelView'; import { messages } from './messages'; +import { NoAssignedEmptyPage } from './noAssignedEmptyPage'; import styles from './organizationsPage.scss'; const cx = classNames.bind(styles); @@ -55,6 +56,9 @@ export const OrganizationsPage = () => { getStorageItem(`${userId}_settings`)?.organizationsPanel === TABLE_VIEW, ); + const assignedOrganizations = useSelector(assignedOrganizationsSelector); + const noAssignedOrganizations = Object.keys(assignedOrganizations).length === 0 && !hasPermission; + const openPanelView = () => { setIsOpenTableView(false); updateStorageItem(`${userId}_settings`, { organizationsPanel: PANEL_VIEW }); @@ -74,6 +78,10 @@ export const OrganizationsPage = () => { ); } + if (noAssignedOrganizations) { + return ; + } + return searchValue === null && appliedFiltersCount === 0 ? ( { return (
- + {!noAssignedOrganizations && ( + + )} {isEmptyOrganizations ? ( getEmptyPageState() ) : ( diff --git a/app/src/pages/instance/organizationsPage/organizationsPageHeader/index.js b/app/src/pages/instance/organizationsPage/organizationsPageHeader/index.js index d271e1ac82..fba0ee003b 100644 --- a/app/src/pages/instance/organizationsPage/organizationsPageHeader/index.js +++ b/app/src/pages/instance/organizationsPage/organizationsPageHeader/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * 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.