Skip to content

Commit

Permalink
EPMRPP-94014 || Empty state for user without any assignments (#4129)
Browse files Browse the repository at this point in the history
* EPMRPP-94014 || Empty state for user without any assignments

* EPMRPP-94014 || fix conditions

* EPMRPP-94014 || fix permission

* EPMRPP-94014 || Code Review fix - 1

* EPMRPP-94014 || Code Review fix - 2
  • Loading branch information
BlazarQSO authored Dec 24, 2024
1 parent 1fc554a commit 1f8a18b
Show file tree
Hide file tree
Showing 19 changed files with 518 additions and 85 deletions.
9 changes: 9 additions & 0 deletions app/localization/translated/be.json
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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": "Няма даступных дадзеных..",
Expand Down
9 changes: 9 additions & 0 deletions app/localization/translated/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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.",
Expand Down
9 changes: 9 additions & 0 deletions app/localization/translated/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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": "Нет доступных данных.",
Expand Down
9 changes: 9 additions & 0 deletions app/localization/translated/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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": "Доступних даних Немає.",
Expand Down
9 changes: 9 additions & 0 deletions app/localization/translated/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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": "无可用数据。",
Expand Down
2 changes: 1 addition & 1 deletion app/src/controllers/initialData/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
9 changes: 6 additions & 3 deletions app/src/controllers/project/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
123 changes: 69 additions & 54 deletions app/src/controllers/user/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }) {
Expand Down
1 change: 1 addition & 0 deletions app/src/controllers/user/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) => {
Expand Down Expand Up @@ -149,7 +155,7 @@ export const InstanceSidebar = ({ onClickNavBtn }) => {

return (
<AppSidebar
createMainBlock={createMainBlock}
createMainBlock={noAssignedOrganizations ? () => {} : createMainBlock}
items={getSidebarItems()}
isOpenOrganizationPopover={isOpenOrganizationPopover}
linkToUserProfilePage={linkToUserProfilePage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,21 @@ export const OrganizationsPopover = ({ closePopover, closeSidebar }) => {
return (
<div className={cx('organizations-popover')}>
<div className={cx('organizations-search')}>
<ThemeProvider theme="dark">
<FieldText
startIcon={<SearchIcon />}
placeholder={formatMessage(COMMON_LOCALE_KEYS.SEARCH)}
className={cx('field-text')}
defaultWidth={false}
value={valueSearch}
onChange={handleChange}
onClear={handleClear}
maxLength={256}
clearable
/>
</ThemeProvider>
{filteredProjects.length > 0 && (
<ThemeProvider theme="dark">
<FieldText
startIcon={<SearchIcon />}
placeholder={formatMessage(COMMON_LOCALE_KEYS.SEARCH)}
className={cx('field-text')}
defaultWidth={false}
value={valueSearch}
onChange={handleChange}
onClear={handleClear}
maxLength={256}
clearable
/>
</ThemeProvider>
)}
</div>
{availableProjects.length > 0 && (
<>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 1f8a18b

Please sign in to comment.