Browse Source

[NO JIRA] Improve background tasks and account RTL ITs

tags/9.8.0.63668
Mathieu Suen 1 year ago
parent
commit
2d5f569fbd

+ 1
- 2
server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts View File

*/ */
import { differenceInMilliseconds, isAfter, isBefore } from 'date-fns'; import { differenceInMilliseconds, isAfter, isBefore } from 'date-fns';
import { cloneDeep, groupBy, sortBy } from 'lodash'; import { cloneDeep, groupBy, sortBy } from 'lodash';
import { PAGE_SIZE } from '../../apps/background-tasks/constants';
import { parseDate } from '../../helpers/dates'; import { parseDate } from '../../helpers/dates';
import { mockTask } from '../../helpers/mocks/tasks'; import { mockTask } from '../../helpers/mocks/tasks';
import { ActivityRequestParameters, Task, TaskStatuses, TaskTypes } from '../../types/tasks'; import { ActivityRequestParameters, Task, TaskStatuses, TaskTypes } from '../../types/tasks';
const RANDOM_RADIX = 36; const RANDOM_RADIX = 36;
const RANDOM_PREFIX = 2; const RANDOM_PREFIX = 2;


const PAGE_SIZE = 100;

const TASK_TYPES = [ const TASK_TYPES = [
TaskTypes.Report, TaskTypes.Report,
TaskTypes.IssueSync, TaskTypes.IssueSync,

+ 4
- 12
server/sonar-web/src/main/js/api/mocks/NotificationsMock.ts View File

import { import {
AddRemoveNotificationParameters, AddRemoveNotificationParameters,
Notification, Notification,
NotificationGlobalType,
NotificationProjectType,
NotificationsResponse, NotificationsResponse,
} from '../../types/notifications'; } from '../../types/notifications';
import { addNotification, getNotifications, removeNotification } from '../notifications'; import { addNotification, getNotifications, removeNotification } from '../notifications';


/* Constants */ /* Constants */
const channels = ['EmailNotificationChannel']; const channels = ['EmailNotificationChannel'];
const globalTypes = ['CeReportTaskFailure', 'ChangesOnMyIssue', 'NewAlerts', 'SQ-MyNewIssues'];
const perProjectTypes = [
'CeReportTaskFailure',
'ChangesOnMyIssue',
'NewAlerts',
'NewFalsePositiveIssue',
'NewIssues',
'SQ-MyNewIssues',
];

const defaultNotifications: Notification[] = [ const defaultNotifications: Notification[] = [
{ channel: 'EmailNotificationChannel', type: 'ChangesOnMyIssue' }, { channel: 'EmailNotificationChannel', type: 'ChangesOnMyIssue' },
]; ];
handleGetNotifications: () => Promise<NotificationsResponse> = () => { handleGetNotifications: () => Promise<NotificationsResponse> = () => {
return Promise.resolve({ return Promise.resolve({
channels: [...channels], channels: [...channels],
globalTypes: [...globalTypes],
globalTypes: Object.values(NotificationGlobalType),
notifications: cloneDeep(this.notifications), notifications: cloneDeep(this.notifications),
perProjectTypes: [...perProjectTypes],
perProjectTypes: Object.values(NotificationProjectType),
}); });
}; };



+ 6
- 2
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx View File

import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { mockComponent } from '../../../../../../../helpers/mocks/component'; import { mockComponent } from '../../../../../../../helpers/mocks/component';
import {
NotificationGlobalType,
NotificationProjectType,
} from '../../../../../../../types/notifications';
import { ProjectNotifications } from '../ProjectNotifications'; import { ProjectNotifications } from '../ProjectNotifications';


jest.mock('react', () => { jest.mock('react', () => {
addNotification={jest.fn()} addNotification={jest.fn()}
channels={['channel1', 'channel2']} channels={['channel1', 'channel2']}
component={mockComponent({ key: 'foo' })} component={mockComponent({ key: 'foo' })}
globalTypes={['type-global', 'type-common']}
globalTypes={[NotificationGlobalType.CeReportTaskFailure, NotificationGlobalType.NewAlerts]}
loading={false} loading={false}
notifications={[ notifications={[
{ {
projectName: 'Qux', projectName: 'Qux',
}, },
]} ]}
perProjectTypes={['type-common']}
perProjectTypes={[NotificationProjectType.CeReportTaskFailure]}
removeNotification={jest.fn()} removeNotification={jest.fn()}
{...props} {...props}
/> />

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap View File

project={true} project={true}
types={ types={
Array [ Array [
"type-common",
"CeReportTaskFailure",
] ]
} }
/> />

+ 45
- 55
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { screen, waitFor, within } from '@testing-library/react';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup'; import { UserEvent } from '@testing-library/user-event/dist/types/setup';
import selectEvent from 'react-select-event'; import selectEvent from 'react-select-event';
import { byLabelText, byRole, byText } from 'testing-library-selector';
import { getMyProjects, getScannableProjects } from '../../../api/components'; import { getMyProjects, getScannableProjects } from '../../../api/components';
import NotificationsMock from '../../../api/mocks/NotificationsMock'; import NotificationsMock from '../../../api/mocks/NotificationsMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock'; import UserTokensMock from '../../../api/mocks/UserTokensMock';
import { setKeyboardShortcutEnabled } from '../../../helpers/preferences'; import { setKeyboardShortcutEnabled } from '../../../helpers/preferences';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { NotificationGlobalType, NotificationProjectType } from '../../../types/notifications';
import { Permissions } from '../../../types/permissions'; import { Permissions } from '../../../types/permissions';
import { TokenType } from '../../../types/token'; import { TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users'; import { CurrentUser } from '../../../types/users';
}); });


describe('notifications page', () => { describe('notifications page', () => {
const projectUI = {
title: byRole('button', { name: 'my_profile.per_project_notifications.add' }),
addButton: byRole('button', { name: 'my_profile.per_project_notifications.add' }),
addModalButton: byRole('button', { name: 'add_verb' }),
searchInput: byLabelText('search_verb', { selector: 'input' }),
sonarQubeProject: byRole('heading', { name: 'SonarQube' }),
checkbox: (type: NotificationProjectType) =>
byRole('checkbox', {
name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}.project`,
}),
};

const globalUI = {
title: byRole('heading', { name: 'my_profile.overall_notifications.title' }),
noNotificationForProject: byText('my_account.no_project_notifications'),
checkbox: (type: NotificationGlobalType) =>
byRole('checkbox', {
name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}`,
}),
};

let notificationsMock: NotificationsMock; let notificationsMock: NotificationsMock;
beforeAll(() => { beforeAll(() => {
notificationsMock = new NotificationsMock(); notificationsMock = new NotificationsMock();
const notificationsPagePath = 'account/notifications'; const notificationsPagePath = 'account/notifications';


it('should display global notifications status and allow edits', async () => { it('should display global notifications status and allow edits', async () => {
const user = userEvent.setup();
const user = userEvent.setup({ delay: null });


renderAccountApp(mockLoggedInUser(), notificationsPagePath); renderAccountApp(mockLoggedInUser(), notificationsPagePath);


expect(
await screen.findByRole('heading', { name: 'my_profile.overall_notifications.title' })
).toBeInTheDocument();

expect(screen.getAllByRole('row')).toHaveLength(5); // 4 + header
expect(await globalUI.title.find()).toBeInTheDocument();


/* /*
* Verify Checkbox statuses * Verify Checkbox statuses
*/ */
expect(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue')).toBeChecked();

// first row is the header: skip it!
const otherRows = screen
.getAllByRole('row', {
name: (n: string) => n !== 'notification.dispatcher.ChangesOnMyIssue',
})
.slice(1);

otherRows.forEach((row) => {
expect(within(row).getByRole('checkbox')).not.toBeChecked();
});

// Make sure the second block is empty
expect(screen.getByText('my_account.no_project_notifications')).toBeInTheDocument();
expect(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get()).toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.CeReportTaskFailure).get()).not.toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.NewAlerts).get()).not.toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.MyNewIssues).get()).not.toBeChecked();


/* /*
* Update notifications * Update notifications
*/ */
await user.click(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue'));
expect(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue')).not.toBeChecked();
await user.click(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get());
expect(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get()).not.toBeChecked();


await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts')).toBeChecked();
await user.click(globalUI.checkbox(NotificationGlobalType.NewAlerts).get());
expect(globalUI.checkbox(NotificationGlobalType.NewAlerts).get()).toBeChecked();
}); });


it('should allow adding notifications for a project', async () => { it('should allow adding notifications for a project', async () => {


renderAccountApp(mockLoggedInUser(), notificationsPagePath); renderAccountApp(mockLoggedInUser(), notificationsPagePath);


await user.click(
await screen.findByRole('button', { name: 'my_profile.per_project_notifications.add' })
);

expect(await screen.findByLabelText('search_verb', { selector: 'input' })).toBeInTheDocument();

expect(screen.getByRole('button', { name: 'add_verb' })).toBeDisabled();

await user.keyboard('sonar');
await user.click(await projectUI.addButton.find());
expect(projectUI.addModalButton.get()).toBeDisabled();
await user.type(projectUI.searchInput.get(), 'sonar');
// navigate within the two results, choose the first: // navigate within the two results, choose the first:
await user.keyboard('[ArrowDown][ArrowDown][ArrowUp][Enter]'); await user.keyboard('[ArrowDown][ArrowDown][ArrowUp][Enter]');
await user.click(projectUI.addModalButton.get());


const addButton = screen.getByRole('button', { name: 'add_verb' });
expect(addButton).toBeEnabled();

await user.click(addButton);

expect(screen.getByRole('heading', { name: 'SonarQube' })).toBeInTheDocument();
expect(projectUI.sonarQubeProject.get()).toBeInTheDocument();
expect( expect(
getCheckboxByRowName('notification.dispatcher.NewFalsePositiveIssue.project')
projectUI.checkbox(NotificationProjectType.NewFalsePositiveIssue).get()
).toBeInTheDocument(); ).toBeInTheDocument();


await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts.project'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts.project')).toBeChecked();

expect(screen.getAllByRole('checkbox', { checked: true })).toHaveLength(2);
await user.click(projectUI.checkbox(NotificationProjectType.NewAlerts).get());
expect(projectUI.checkbox(NotificationProjectType.NewAlerts).get()).toBeChecked();


await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts.project'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts.project')).not.toBeChecked();
await user.click(projectUI.checkbox(NotificationProjectType.NewAlerts).get());
expect(projectUI.checkbox(NotificationProjectType.NewAlerts).get()).not.toBeChecked();
}); });


it('should allow searching for projects', async () => { it('should allow searching for projects', async () => {
await user.click(screen.getByRole('searchbox')); await user.click(screen.getByRole('searchbox'));
await user.keyboard('bla'); await user.keyboard('bla');


await waitFor(() => {
expect(screen.queryByRole('heading', { name: 'SonarQube' })).not.toBeInTheDocument();
});
expect(screen.queryByRole('heading', { name: 'SonarQube' })).not.toBeInTheDocument();


await user.keyboard('[Backspace>3/]'); await user.keyboard('[Backspace>3/]');


return result; return result;
} }


function getCheckboxByRowName(name: string) {
return within(screen.getByRole('row', { name })).getByRole('checkbox');
}

function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) { function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) {
renderAppRoutes('account', routes, { currentUser, navigateTo }); renderAppRoutes('account', routes, { currentUser, navigateTo });
} }

+ 2
- 2
server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx View File

*/ */
import * as React from 'react'; import * as React from 'react';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { Notification } from '../../../types/notifications';
import { Notification, NotificationGlobalType } from '../../../types/notifications';
import NotificationsList from './NotificationsList'; import NotificationsList from './NotificationsList';


interface Props { interface Props {
channels: string[]; channels: string[];
notifications: Notification[]; notifications: Notification[];
removeNotification: (n: Notification) => void; removeNotification: (n: Notification) => void;
types: string[];
types: NotificationGlobalType[];
} }


export default function GlobalNotifications(props: Props) { export default function GlobalNotifications(props: Props) {

+ 11
- 3
server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx View File

*/ */
import * as React from 'react'; import * as React from 'react';
import Checkbox from '../../../components/controls/Checkbox'; import Checkbox from '../../../components/controls/Checkbox';
import { hasMessage, translate } from '../../../helpers/l10n';
import { Notification } from '../../../types/notifications';
import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n';
import {
Notification,
NotificationGlobalType,
NotificationProjectType,
} from '../../../types/notifications';


interface Props { interface Props {
onAdd: (n: Notification) => void; onAdd: (n: Notification) => void;
channels: string[]; channels: string[];
checkboxId: (type: string, channel: string) => string; checkboxId: (type: string, channel: string) => string;
project?: boolean; project?: boolean;
types: string[];
types: (NotificationGlobalType | NotificationProjectType)[];
notifications: Notification[]; notifications: Notification[];
} }


{channels.map((channel) => ( {channels.map((channel) => (
<td className="text-center" key={channel}> <td className="text-center" key={channel}>
<Checkbox <Checkbox
label={translateWithParameters(
'notification.dispatcher.descrption_x',
this.getDispatcherLabel(type)
)}
checked={this.isEnabled(type, channel)} checked={this.isEnabled(type, channel)}
id={checkboxId(type, channel)} id={checkboxId(type, channel)}
onCheck={(checked) => this.handleCheck(type, channel, checked)} onCheck={(checked) => this.handleCheck(type, channel, checked)}

+ 6
- 2
server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx View File

import * as React from 'react'; import * as React from 'react';
import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { Notification, NotificationProject } from '../../../types/notifications';
import {
Notification,
NotificationProject,
NotificationProjectType,
} from '../../../types/notifications';
import NotificationsList from './NotificationsList'; import NotificationsList from './NotificationsList';


interface Props { interface Props {
notifications: Notification[]; notifications: Notification[];
project: NotificationProject; project: NotificationProject;
removeNotification: (n: Notification) => void; removeNotification: (n: Notification) => void;
types: string[];
types: NotificationProjectType[];
} }


export default function ProjectNotifications(props: Props) { export default function ProjectNotifications(props: Props) {

+ 6
- 2
server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx View File

import { Button } from '../../../components/controls/buttons'; import { Button } from '../../../components/controls/buttons';
import SearchBox from '../../../components/controls/SearchBox'; import SearchBox from '../../../components/controls/SearchBox';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { Notification, NotificationProject } from '../../../types/notifications';
import {
Notification,
NotificationProject,
NotificationProjectType,
} from '../../../types/notifications';
import ProjectModal from './ProjectModal'; import ProjectModal from './ProjectModal';
import ProjectNotifications from './ProjectNotifications'; import ProjectNotifications from './ProjectNotifications';


channels: string[]; channels: string[];
notifications: Notification[]; notifications: Notification[];
removeNotification: (n: Notification) => void; removeNotification: (n: Notification) => void;
types: string[];
types: NotificationProjectType[];
} }


const THRESHOLD_COLLAPSED = 3; const THRESHOLD_COLLAPSED = 3;

+ 9
- 3
server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx View File



jest.mock('../../../api/ce'); jest.mock('../../../api/ce');


jest.mock('../constants', () => {
const contants = jest.requireActual('../constants');

return { ...contants, PAGE_SIZE: 9 };
});

let computeEngineServiceMock: ComputeEngineServiceMock; let computeEngineServiceMock: ComputeEngineServiceMock;


beforeAll(() => { beforeAll(() => {
const user = userEvent.setup(); const user = userEvent.setup();


computeEngineServiceMock.clearTasks(); computeEngineServiceMock.clearTasks();
computeEngineServiceMock.createTasks(101);
computeEngineServiceMock.createTasks(10);


renderGlobalBackgroundTasksApp(); renderGlobalBackgroundTasksApp();


await screen.findByRole('heading', { name: 'background_tasks.page' }) await screen.findByRole('heading', { name: 'background_tasks.page' })
).toBeInTheDocument(); ).toBeInTheDocument();


expect(screen.getAllByRole('row')).toHaveLength(101); // including header
expect(screen.getAllByRole('row')).toHaveLength(10); // including header


user.click(screen.getByRole('button', { name: 'show_more' })); user.click(screen.getByRole('button', { name: 'show_more' }));


await waitFor(() => { await waitFor(() => {
expect(screen.getAllByRole('row')).toHaveLength(102); // including header
expect(screen.getAllByRole('row')).toHaveLength(11); // including header
}); });
}); });



+ 1
- 3
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx View File

import { Task, TaskStatuses } from '../../../types/tasks'; import { Task, TaskStatuses } from '../../../types/tasks';
import { Component, Paging, RawQuery } from '../../../types/types'; import { Component, Paging, RawQuery } from '../../../types/types';
import '../background-tasks.css'; import '../background-tasks.css';
import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS } from '../constants';
import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS, PAGE_SIZE } from '../constants';
import { mapFiltersToParameters, Query, updateTask } from '../utils'; import { mapFiltersToParameters, Query, updateTask } from '../utils';
import Header from './Header'; import Header from './Header';
import Search from './Search'; import Search from './Search';
types?: string[]; types?: string[];
} }


const PAGE_SIZE = 100;

export class BackgroundTasksApp extends React.PureComponent<Props, State> { export class BackgroundTasksApp extends React.PureComponent<Props, State> {
loadTasksDebounced: () => void; loadTasksDebounced: () => void;
mounted = false; mounted = false;

+ 2
- 0
server/sonar-web/src/main/js/apps/background-tasks/constants.ts View File

export const DATE_FORMAT = 'YYYY-MM-DD'; export const DATE_FORMAT = 'YYYY-MM-DD';


export const DEBOUNCE_DELAY = 250; export const DEBOUNCE_DELAY = 250;

export const PAGE_SIZE = 100;

+ 9
- 5
server/sonar-web/src/main/js/components/hoc/withNotifications.tsx View File

import { uniqWith } from 'lodash'; import { uniqWith } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { addNotification, getNotifications, removeNotification } from '../../api/notifications'; import { addNotification, getNotifications, removeNotification } from '../../api/notifications';
import { Notification } from '../../types/notifications';
import {
Notification,
NotificationGlobalType,
NotificationProjectType,
} from '../../types/notifications';
import { getWrappedDisplayName } from './utils'; import { getWrappedDisplayName } from './utils';


interface State { interface State {
channels: string[]; channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
loading: boolean; loading: boolean;
notifications: Notification[]; notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
} }


export interface WithNotificationsProps { export interface WithNotificationsProps {
addNotification: (added: Notification) => void; addNotification: (added: Notification) => void;
channels: string[]; channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
loading: boolean; loading: boolean;
notifications: Notification[]; notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
removeNotification: (removed: Notification) => void; removeNotification: (removed: Notification) => void;
} }



+ 19
- 2
server/sonar-web/src/main/js/types/notifications.ts View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

export enum NotificationGlobalType {
CeReportTaskFailure = 'CeReportTaskFailure',
ChangesOnMyIssue = 'ChangesOnMyIssue',
NewAlerts = 'NewAlerts',
MyNewIssues = 'SQ-MyNewIssues',
}

export enum NotificationProjectType {
CeReportTaskFailure = 'CeReportTaskFailure',
ChangesOnMyIssue = 'ChangesOnMyIssue',
NewAlerts = 'NewAlerts',
NewFalsePositiveIssue = 'NewFalsePositiveIssue',
NewIssues = 'NewIssues',
MyNewIssues = 'SQ-MyNewIssues',
}

export interface Notification { export interface Notification {
channel: string; channel: string;
project?: string; project?: string;


export interface NotificationsResponse { export interface NotificationsResponse {
channels: string[]; channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
notifications: Notification[]; notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
} }


export interface AddRemoveNotificationParameters { export interface AddRemoveNotificationParameters {

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

notification.dispatcher.SQ-MyNewIssues=My new issues notification.dispatcher.SQ-MyNewIssues=My new issues
notification.dispatcher.CeReportTaskFailure=Background tasks in failure on my administered projects notification.dispatcher.CeReportTaskFailure=Background tasks in failure on my administered projects
notification.dispatcher.CeReportTaskFailure.project=Background tasks in failure notification.dispatcher.CeReportTaskFailure.project=Background tasks in failure
notification.dispatcher.descrption_x=Check to receive notification for {0}


#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #

Loading…
Cancel
Save