* Create calendar list in react * wip: create new_calendar page * Update new calendar settings directory name * Edit button action + update utils * Adds ability to create new calendar * Display calendar data on edit * rename directory to settings/calendar * Add scss files to calendar dir * Create new group from form * Adds event table and partial event modal. * adds datepicker to modal * Time range event functionality * add import event functionality * upate new event modal design * Add error handling to list/edit * calendarId validity check * Create/delete permission. List/form style tweak * Update calendarList to match filterList * Add missing newlines in scss files * Initial tests for calendar list * Update classnames to meet guidelines * ImportedEvents component + create utils * remove unnecessary import * rename calendars dir * include past evens in import if checkbox checked * code review updates * move components into own directories * update index.scss with dir name change * skip irrelevant tests * fix unsaved event deletion. rename scss file. * Add modal tests * Show calendarId and description as header on edit * update snapshot for refactor * update classnames to BEM guidelines * Update snapshot for classname change
This commit is contained in:
parent
5b82c0692b
commit
00ab45a26d
|
@ -1,3 +1,3 @@
|
|||
@import 'settings';
|
||||
@import 'filter_lists/index';
|
||||
@import 'scheduled_events/index';
|
||||
@import 'calendars/index';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.mlCalendarManagement {
|
||||
background: $euiColorLightestShade;
|
||||
min-height: 100vh;
|
||||
}
|
3
x-pack/plugins/ml/public/settings/calendars/_index.scss
Normal file
3
x-pack/plugins/ml/public/settings/calendars/_index.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@import 'calendars';
|
||||
@import 'edit/index';
|
||||
@import 'list/index';
|
|
@ -0,0 +1,38 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NewCalendar Renders new calendar form 1`] = `
|
||||
<EuiPage
|
||||
className="mlCalendarEditForm"
|
||||
restrictWidth={false}
|
||||
>
|
||||
<EuiPageContent
|
||||
className="mlCalendarEditForm__content"
|
||||
horizontalPosition="center"
|
||||
panelPaddingSize="l"
|
||||
verticalPosition="center"
|
||||
>
|
||||
<CalendarForm
|
||||
calendarId=""
|
||||
description=""
|
||||
eventsList={Array []}
|
||||
groupIds={Array []}
|
||||
isEdit={false}
|
||||
isNewCalendarIdValid={true}
|
||||
jobIds={Array []}
|
||||
onCalendarIdChange={[Function]}
|
||||
onCreate={[Function]}
|
||||
onCreateGroupOption={[Function]}
|
||||
onDescriptionChange={[Function]}
|
||||
onEdit={[Function]}
|
||||
onEventDelete={[Function]}
|
||||
onGroupSelection={[Function]}
|
||||
onJobSelection={[Function]}
|
||||
saving={false}
|
||||
selectedGroupOptions={Array []}
|
||||
selectedJobOptions={Array []}
|
||||
showImportModal={[Function]}
|
||||
showNewEventModal={[Function]}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
`;
|
|
@ -0,0 +1,8 @@
|
|||
.mlCalendarEditForm {
|
||||
.mlCalendarEditForm__content {
|
||||
max-width: map-get($euiBreakpoints, 'xl');
|
||||
width: 100%;
|
||||
margin-top: $euiSize;
|
||||
margin-bottom: $euiSize;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'edit';
|
|
@ -0,0 +1,142 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CalendarForm Renders calendar form 1`] = `
|
||||
<EuiForm>
|
||||
<React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
error={
|
||||
Array [
|
||||
"Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;
|
||||
must start and end with an alphanumeric character",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
label="Calendar ID"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
name="calendarId"
|
||||
onChange={[MockFunction]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Description"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
name="description"
|
||||
onChange={[MockFunction]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Jobs"
|
||||
>
|
||||
<EuiComboBox
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
isClearable={true}
|
||||
onChange={[MockFunction]}
|
||||
options={Array []}
|
||||
selectedOptions={Array []}
|
||||
singleSelection={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Groups"
|
||||
>
|
||||
<EuiComboBox
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
isClearable={true}
|
||||
onChange={[MockFunction]}
|
||||
onCreateOption={[MockFunction]}
|
||||
options={Array []}
|
||||
selectedOptions={Array []}
|
||||
singleSelection={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Events"
|
||||
>
|
||||
<EventsTable
|
||||
eventsList={Array []}
|
||||
onDeleteClick={[MockFunction]}
|
||||
showImportModal={[MockFunction]}
|
||||
showNewEventModal={[MockFunction]}
|
||||
showSearchBar={true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexEnd"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
disabled={true}
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
disabled={false}
|
||||
fill={false}
|
||||
href="undefined/app/ml#/settings/calendars_list"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
`;
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiComboBox,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { EventsTable } from '../events_table/';
|
||||
|
||||
|
||||
function EditHeader({
|
||||
calendarId,
|
||||
description
|
||||
}) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle>
|
||||
<h1>Calendar {calendarId}</h1>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
{description}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l"/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarForm({
|
||||
calendarId,
|
||||
description,
|
||||
eventsList,
|
||||
groupIds,
|
||||
isEdit,
|
||||
isNewCalendarIdValid,
|
||||
jobIds,
|
||||
onCalendarIdChange,
|
||||
onCreate,
|
||||
onCreateGroupOption,
|
||||
onDescriptionChange,
|
||||
onEdit,
|
||||
onEventDelete,
|
||||
onGroupSelection,
|
||||
showImportModal,
|
||||
onJobSelection,
|
||||
saving,
|
||||
selectedGroupOptions,
|
||||
selectedJobOptions,
|
||||
showNewEventModal
|
||||
}) {
|
||||
const msg = `Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;
|
||||
must start and end with an alphanumeric character`;
|
||||
const helpText = (isNewCalendarIdValid === true && !isEdit) ? msg : undefined;
|
||||
const error = (isNewCalendarIdValid === false && !isEdit) ? [msg] : undefined;
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
{!isEdit &&
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label="Calendar ID"
|
||||
helpText={helpText}
|
||||
error={error}
|
||||
isInvalid={!isNewCalendarIdValid}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="calendarId"
|
||||
value={calendarId}
|
||||
onChange={onCalendarIdChange}
|
||||
disabled={isEdit === true || saving === true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label="Description"
|
||||
>
|
||||
<EuiFieldText
|
||||
name="description"
|
||||
value={description}
|
||||
onChange={onDescriptionChange}
|
||||
disabled={isEdit === true || saving === true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
}
|
||||
{isEdit &&
|
||||
<EditHeader
|
||||
calendarId={calendarId}
|
||||
description={description}
|
||||
/>}
|
||||
<EuiFormRow
|
||||
label="Jobs"
|
||||
>
|
||||
<EuiComboBox
|
||||
options={jobIds}
|
||||
selectedOptions={selectedJobOptions}
|
||||
onChange={onJobSelection}
|
||||
disabled={saving === true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label="Groups"
|
||||
>
|
||||
<EuiComboBox
|
||||
onCreateOption={onCreateGroupOption}
|
||||
options={groupIds}
|
||||
selectedOptions={selectedGroupOptions}
|
||||
onChange={onGroupSelection}
|
||||
disabled={saving === true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiFormRow
|
||||
label="Events"
|
||||
fullWidth
|
||||
>
|
||||
<EventsTable
|
||||
eventsList={eventsList}
|
||||
onDeleteClick={onEventDelete}
|
||||
showImportModal={showImportModal}
|
||||
showNewEventModal={showNewEventModal}
|
||||
showSearchBar
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={isEdit ? onEdit : onCreate}
|
||||
disabled={saving || !isNewCalendarIdValid || calendarId === ''}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
disabled={saving}
|
||||
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list`}
|
||||
>
|
||||
Cancel
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
CalendarForm.propTypes = {
|
||||
calendarId: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
groupIds: PropTypes.array.isRequired,
|
||||
isEdit: PropTypes.bool.isRequired,
|
||||
isNewCalendarIdValid: PropTypes.bool.isRequired,
|
||||
jobIds: PropTypes.array.isRequired,
|
||||
onCalendarIdChange: PropTypes.func.isRequired,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onCreateGroupOption: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onEventDelete: PropTypes.func.isRequired,
|
||||
onGroupSelection: PropTypes.func.isRequired,
|
||||
showImportModal: PropTypes.func.isRequired,
|
||||
onJobSelection: PropTypes.func.isRequired,
|
||||
saving: PropTypes.bool.isRequired,
|
||||
selectedGroupOptions: PropTypes.array.isRequired,
|
||||
selectedJobOptions: PropTypes.array.isRequired,
|
||||
showNewEventModal: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { CalendarForm } from './calendar_form';
|
||||
|
||||
const testProps = {
|
||||
calendarId: '',
|
||||
description: '',
|
||||
eventsList: [],
|
||||
groupIds: [],
|
||||
isEdit: false,
|
||||
isNewCalendarIdValid: false,
|
||||
jobIds: [],
|
||||
onCalendarIdChange: jest.fn(),
|
||||
onCreate: jest.fn(),
|
||||
onCreateGroupOption: jest.fn(),
|
||||
onDescriptionChange: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
onEventDelete: jest.fn(),
|
||||
onGroupSelection: jest.fn(),
|
||||
showImportModal: jest.fn(),
|
||||
onJobSelection: jest.fn(),
|
||||
saving: false,
|
||||
selectedGroupOptions: [],
|
||||
selectedJobOptions: [],
|
||||
showNewEventModal: jest.fn()
|
||||
};
|
||||
|
||||
describe('CalendarForm', () => {
|
||||
|
||||
test('Renders calendar form', () => {
|
||||
const wrapper = shallow(
|
||||
<CalendarForm {...testProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('CalendarId shown as title when editing', () => {
|
||||
const editProps = {
|
||||
...testProps,
|
||||
isEdit: true,
|
||||
calendarId: 'test-calendar',
|
||||
description: 'test description',
|
||||
};
|
||||
const wrapper = mount(
|
||||
<CalendarForm {...editProps} />
|
||||
);
|
||||
const calendarId = wrapper.find('EuiTitle');
|
||||
|
||||
expect(
|
||||
calendarId.containsMatchingElement(
|
||||
<h1>Calendar test-calendar</h1>
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { CalendarForm } from './calendar_form';
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { checkFullLicense } from '../../../license/check_license';
|
||||
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
|
||||
import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
|
||||
import { initPromise } from 'plugins/ml/util/promise';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
||||
const template = `
|
||||
<ml-nav-menu name="settings" />
|
||||
<div class="mlCalendarManagement">
|
||||
<ml-new-calendar />
|
||||
</div>
|
||||
`;
|
||||
|
||||
uiRoutes
|
||||
.when('/settings/calendars_list/new_calendar', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
checkMlNodesAvailable,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
})
|
||||
.when('/settings/calendars_list/edit_calendar/:calendarId', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
checkMlNodesAvailable,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
});
|
||||
|
||||
import { NewCalendar } from './new_calendar.js';
|
||||
|
||||
module.directive('mlNewCalendar', function ($route) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
scope: {},
|
||||
link: function (scope, element) {
|
||||
const props = {
|
||||
calendarId: $route.current.params.calendarId
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(NewCalendar, props),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,169 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EventsTable Renders events table with no search bar 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
"truncateText": true,
|
||||
},
|
||||
Object {
|
||||
"field": "start_time",
|
||||
"name": "Start",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "end_time",
|
||||
"name": "End",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "",
|
||||
"name": "",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
itemId="event_id"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"calendar_id": "test-calendar",
|
||||
"description": "test description",
|
||||
"end_time": 1486657800000,
|
||||
"event_id": "test-event-one",
|
||||
"start_time": 1486656600000,
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={
|
||||
Object {
|
||||
"initialPageSize": 5,
|
||||
"pageSizeOptions": Array [
|
||||
5,
|
||||
10,
|
||||
],
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "description",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`EventsTable Renders events table with search bar 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
"truncateText": true,
|
||||
},
|
||||
Object {
|
||||
"field": "start_time",
|
||||
"name": "Start",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "end_time",
|
||||
"name": "End",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "",
|
||||
"name": "",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
itemId="event_id"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"calendar_id": "test-calendar",
|
||||
"description": "test description",
|
||||
"end_time": 1486657800000,
|
||||
"event_id": "test-event-one",
|
||||
"start_time": 1486656600000,
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={
|
||||
Object {
|
||||
"initialPageSize": 5,
|
||||
"pageSizeOptions": Array [
|
||||
5,
|
||||
10,
|
||||
],
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"box": Object {
|
||||
"incremental": true,
|
||||
},
|
||||
"filters": Array [],
|
||||
"toolsRight": Array [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-testid="ml_new_event"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="plusInCircle"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
New event
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-testid="ml_import_events"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="importAction"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Import events
|
||||
</EuiButton>,
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "description",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiInMemoryTable,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
function DeleteButton({ onClick }) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="danger"
|
||||
onClick={onClick}
|
||||
>
|
||||
Delete
|
||||
</EuiButtonEmpty>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventsTable({
|
||||
eventsList,
|
||||
onDeleteClick,
|
||||
showSearchBar,
|
||||
showImportModal,
|
||||
showNewEventModal
|
||||
}) {
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'description',
|
||||
direction: 'asc',
|
||||
}
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 5,
|
||||
pageSizeOptions: [5, 10]
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
truncateText: true
|
||||
},
|
||||
{
|
||||
field: 'start_time',
|
||||
name: 'Start',
|
||||
sortable: true,
|
||||
render: (timeMs) => {
|
||||
const time = moment(timeMs);
|
||||
return time.format(TIME_FORMAT);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'end_time',
|
||||
name: 'End',
|
||||
sortable: true,
|
||||
render: (timeMs) => {
|
||||
const time = moment(timeMs);
|
||||
return time.format(TIME_FORMAT);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
name: '',
|
||||
render: (event) => (
|
||||
<DeleteButton
|
||||
data-testid="event_delete"
|
||||
onClick={() => { onDeleteClick(event.event_id); }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const search = {
|
||||
toolsRight: [(
|
||||
<EuiButton
|
||||
key="ml_new_event"
|
||||
data-testid="ml_new_event"
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={showNewEventModal}
|
||||
>
|
||||
New event
|
||||
</EuiButton>),
|
||||
(
|
||||
<EuiButton
|
||||
key="ml_import_event"
|
||||
data-testid="ml_import_events"
|
||||
size="s"
|
||||
iconType="importAction"
|
||||
onClick={showImportModal}
|
||||
>
|
||||
Import events
|
||||
</EuiButton>
|
||||
)],
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: []
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiInMemoryTable
|
||||
items={eventsList}
|
||||
itemId="event_id"
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
search={showSearchBar ? search : undefined}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
EventsTable.propTypes = {
|
||||
eventsList: PropTypes.array.isRequired,
|
||||
onDeleteClick: PropTypes.func.isRequired,
|
||||
showImportModal: PropTypes.func,
|
||||
showNewEventModal: PropTypes.func,
|
||||
showSearchBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
EventsTable.defaultProps = {
|
||||
showSearchBar: false,
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { EventsTable } from './events_table';
|
||||
|
||||
const testProps = {
|
||||
eventsList: [{
|
||||
calendar_id: 'test-calendar',
|
||||
description: 'test description',
|
||||
start_time: 1486656600000,
|
||||
end_time: 1486657800000,
|
||||
event_id: 'test-event-one'
|
||||
}],
|
||||
onDeleteClick: jest.fn(),
|
||||
showSearchBar: false,
|
||||
showImportModal: jest.fn(),
|
||||
showNewEventModal: jest.fn()
|
||||
};
|
||||
|
||||
describe('EventsTable', () => {
|
||||
|
||||
test('Renders events table with no search bar', () => {
|
||||
const wrapper = shallow(
|
||||
<EventsTable {...testProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renders events table with search bar', () => {
|
||||
const showSearchBarProps = {
|
||||
...testProps,
|
||||
showSearchBar: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<EventsTable {...showSearchBarProps} />
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { EventsTable, TIME_FORMAT } from './events_table';
|
|
@ -0,0 +1,82 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportModal Renders import modal 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[MockFunction]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiModalHeaderTitle>
|
||||
Import events
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<p>
|
||||
Import events from an ICS file.
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="column"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFilePicker
|
||||
compressed={true}
|
||||
disabled={false}
|
||||
initialPromptText="Select or drag and drop a file"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
disabled={true}
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Import
|
||||
</EuiButton>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</React.Fragment>
|
||||
`;
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import React, {
|
||||
Component,
|
||||
Fragment
|
||||
} from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFilePicker,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ImportedEvents } from '../imported_events';
|
||||
import { readFile, parseICSFile, filterEvents } from './utils';
|
||||
|
||||
const MAX_FILE_SIZE_MB = 100;
|
||||
|
||||
export class ImportModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
includePastEvents: false,
|
||||
allImportedEvents: [],
|
||||
selectedEvents: [],
|
||||
fileLoading: false,
|
||||
fileLoaded: false,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
handleImport = async (loadedFile) => {
|
||||
const incomingFile = loadedFile[0];
|
||||
const errorMessage = 'Could not parse ICS file.';
|
||||
let events = [];
|
||||
|
||||
if (incomingFile && incomingFile.size <= (MAX_FILE_SIZE_MB * 1000000)) {
|
||||
this.setState({ fileLoading: true, fileLoaded: true });
|
||||
|
||||
try {
|
||||
const parsedFile = await readFile(incomingFile);
|
||||
events = parseICSFile(parsedFile.data);
|
||||
|
||||
this.setState({
|
||||
allImportedEvents: events,
|
||||
selectedEvents: filterEvents(events),
|
||||
fileLoading: false,
|
||||
errorMessage: null,
|
||||
includePastEvents: false
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(errorMessage, error);
|
||||
this.setState({ errorMessage, fileLoading: false });
|
||||
}
|
||||
} else if (incomingFile && incomingFile.size > (MAX_FILE_SIZE_MB * 1000000)) {
|
||||
this.setState({ fileLoading: false, errorMessage });
|
||||
} else {
|
||||
this.setState({ fileLoading: false, errorMessage: null });
|
||||
}
|
||||
}
|
||||
|
||||
onEventDelete = (eventId) => {
|
||||
this.setState(prevState => ({
|
||||
allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId),
|
||||
selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId),
|
||||
}));
|
||||
}
|
||||
|
||||
onCheckboxToggle = (e) => {
|
||||
this.setState({
|
||||
includePastEvents: e.target.checked,
|
||||
});
|
||||
};
|
||||
|
||||
handleEventsAdd = () => {
|
||||
const { allImportedEvents, selectedEvents, includePastEvents } = this.state;
|
||||
const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents;
|
||||
|
||||
const events = eventsToImport.map((event) => ({
|
||||
description: event.description,
|
||||
start_time: event.start_time,
|
||||
end_time: event.end_time,
|
||||
event_id: event.event_id
|
||||
}));
|
||||
|
||||
this.props.addImportedEvents(events);
|
||||
}
|
||||
|
||||
renderCallout = () => (
|
||||
<EuiCallOut color="danger">
|
||||
<p>{this.state.errorMessage}</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { closeImportModal } = this.props;
|
||||
const {
|
||||
fileLoading,
|
||||
fileLoaded,
|
||||
allImportedEvents,
|
||||
selectedEvents,
|
||||
errorMessage,
|
||||
includePastEvents
|
||||
} = this.state;
|
||||
|
||||
let showRecurringWarning = false;
|
||||
let importedEvents;
|
||||
|
||||
if (includePastEvents) {
|
||||
importedEvents = allImportedEvents;
|
||||
} else {
|
||||
importedEvents = selectedEvents;
|
||||
}
|
||||
|
||||
if (importedEvents.find(e => e.asterisk) !== undefined) {
|
||||
showRecurringWarning = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiModal
|
||||
onClose={closeImportModal}
|
||||
maxWidth={true}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiModalHeaderTitle >
|
||||
Import events
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<p>Import events from an ICS file.</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFilePicker
|
||||
compressed
|
||||
initialPromptText="Select or drag and drop a file"
|
||||
onChange={this.handleImport}
|
||||
disabled={fileLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{errorMessage !== null && this.renderCallout()}
|
||||
{
|
||||
allImportedEvents.length > 0 &&
|
||||
<ImportedEvents
|
||||
events={importedEvents}
|
||||
showRecurringWarning={showRecurringWarning}
|
||||
includePastEvents={includePastEvents}
|
||||
onCheckboxToggle={this.onCheckboxToggle}
|
||||
onEventDelete={this.onEventDelete}
|
||||
/>
|
||||
}
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
onClick={this.handleEventsAdd}
|
||||
fill
|
||||
disabled={fileLoaded === false || errorMessage !== null}
|
||||
>
|
||||
Import
|
||||
</EuiButton>
|
||||
<EuiButtonEmpty
|
||||
onClick={closeImportModal}
|
||||
>
|
||||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportModal.propTypes = {
|
||||
addImportedEvents: PropTypes.func.isRequired,
|
||||
closeImportModal: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ImportModal } from './import_modal';
|
||||
|
||||
const testProps = {
|
||||
addImportedEvents: jest.fn(),
|
||||
closeImportModal: jest.fn()
|
||||
};
|
||||
|
||||
const events = [{
|
||||
'description': 'Downtime feb 9 2017 10:10 to 10:30',
|
||||
'start_time': 1486656600000,
|
||||
'end_time': 1486657800000,
|
||||
'calendar_id': 'farequote-calendar',
|
||||
'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
|
||||
},
|
||||
{
|
||||
'description': 'New event!',
|
||||
'start_time': 1544076000000,
|
||||
'end_time': 1544162400000,
|
||||
'calendar_id': 'this-is-a-new-calendar',
|
||||
'event_id': 'ehWKhGcBqHkXuWNrIrSV'
|
||||
}];
|
||||
|
||||
describe('ImportModal', () => {
|
||||
|
||||
test('Renders import modal', () => {
|
||||
const wrapper = shallow(
|
||||
<ImportModal {...testProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Deletes selected event from event table', () => {
|
||||
const wrapper = mount(
|
||||
<ImportModal {...testProps} />
|
||||
);
|
||||
|
||||
const testState = {
|
||||
allImportedEvents: events,
|
||||
selectedEvents: events,
|
||||
};
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.setState(testState);
|
||||
wrapper.update();
|
||||
expect(wrapper.state('selectedEvents').length).toBe(2);
|
||||
const deleteButton = wrapper.find('[data-testid="event_delete"]');
|
||||
const button = deleteButton.find('EuiButtonEmpty').first();
|
||||
button.simulate('click');
|
||||
|
||||
expect(wrapper.state('selectedEvents').length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { ImportModal } from './import_modal';
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
const icalendar = require('icalendar');
|
||||
import moment from 'moment';
|
||||
import { generateTempId } from '../utils';
|
||||
|
||||
|
||||
function createEvents(ical) {
|
||||
const events = ical.events();
|
||||
const mlEvents = [];
|
||||
|
||||
events.forEach((e) => {
|
||||
if (e.element === 'VEVENT') {
|
||||
const description = e.properties.SUMMARY;
|
||||
const start = e.properties.DTSTART;
|
||||
const end = e.properties.DTEND;
|
||||
const recurring = (e.properties.RRULE !== undefined);
|
||||
|
||||
if (description && start && end && description.length && start.length && end.length) {
|
||||
// Temp reference to unsaved events to allow removal from table
|
||||
const tempId = generateTempId();
|
||||
|
||||
mlEvents.push({
|
||||
event_id: tempId,
|
||||
description: description[0].value,
|
||||
start_time: start[0].value.valueOf(),
|
||||
end_time: end[0].value.valueOf(),
|
||||
asterisk: recurring
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return mlEvents;
|
||||
}
|
||||
|
||||
export function filterEvents(events) {
|
||||
const now = moment().valueOf();
|
||||
return events.filter(e => e.start_time > now);
|
||||
}
|
||||
|
||||
export function parseICSFile(data) {
|
||||
const cal = icalendar.parse_calendar(data);
|
||||
return createEvents(cal);
|
||||
}
|
||||
|
||||
export function readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file && file.size) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
|
||||
reader.onload = (() => {
|
||||
return () => {
|
||||
const data = reader.result;
|
||||
if (data === '') {
|
||||
reject();
|
||||
} else {
|
||||
resolve({ data });
|
||||
}
|
||||
};
|
||||
})(file);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportedEvents Renders imported events 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiText
|
||||
grow={true}
|
||||
size="m"
|
||||
>
|
||||
<h4>
|
||||
Events to import:
|
||||
1
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EventsTable
|
||||
eventsList={
|
||||
Array [
|
||||
Object {
|
||||
"calendar_id": "test-calendar",
|
||||
"description": "test description",
|
||||
"end_time": 1486657800000,
|
||||
"event_id": "test-event-one",
|
||||
"start_time": 1486656600000,
|
||||
},
|
||||
]
|
||||
}
|
||||
onDeleteClick={[MockFunction]}
|
||||
showSearchBar={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiCheckbox
|
||||
checked={false}
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
id="ml-include-past-events"
|
||||
indeterminate={false}
|
||||
label="Include past events"
|
||||
onChange={[MockFunction]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
`;
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import {
|
||||
EuiCheckbox,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiSpacer
|
||||
} from '@elastic/eui';
|
||||
import { EventsTable } from '../events_table/';
|
||||
|
||||
|
||||
export function ImportedEvents({
|
||||
events,
|
||||
showRecurringWarning,
|
||||
includePastEvents,
|
||||
onCheckboxToggle,
|
||||
onEventDelete,
|
||||
}) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s"/>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h4>Events to import: {events.length}</h4>
|
||||
{showRecurringWarning && (
|
||||
<EuiText color="danger">
|
||||
<p>Recurring events not supported. Only the first event will be imported.</p>
|
||||
</EuiText>)
|
||||
}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EventsTable
|
||||
eventsList={events}
|
||||
onDeleteClick={onEventDelete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id="ml-include-past-events"
|
||||
label="Include past events"
|
||||
checked={includePastEvents}
|
||||
onChange={onCheckboxToggle}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ImportedEvents.propTypes = {
|
||||
events: PropTypes.array.isRequired,
|
||||
showRecurringWarning: PropTypes.bool.isRequired,
|
||||
includePastEvents: PropTypes.bool.isRequired,
|
||||
onCheckboxToggle: PropTypes.func.isRequired,
|
||||
onEventDelete: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ImportedEvents } from './imported_events';
|
||||
|
||||
const testProps = {
|
||||
events: [{
|
||||
calendar_id: 'test-calendar',
|
||||
description: 'test description',
|
||||
start_time: 1486656600000,
|
||||
end_time: 1486657800000,
|
||||
event_id: 'test-event-one'
|
||||
}],
|
||||
showRecurringWarning: false,
|
||||
includePastEvents: false,
|
||||
onCheckboxToggle: jest.fn(),
|
||||
onEventDelete: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ImportedEvents', () => {
|
||||
|
||||
test('Renders imported events', () => {
|
||||
const wrapper = shallow(
|
||||
<ImportedEvents {...testProps} />
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ImportedEvents } from './imported_events';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './directive';
|
317
x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.js
Normal file
317
x-pack/plugins/ml/public/settings/calendars/edit/new_calendar.js
Normal file
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React, {
|
||||
Component
|
||||
} from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageContent,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { getCalendarSettingsData, validateCalendarId } from './utils';
|
||||
import { CalendarForm } from './calendar_form/';
|
||||
import { NewEventModal } from './new_event_modal/';
|
||||
import { ImportModal } from './import_modal';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
export class NewCalendar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isNewEventModalVisible: false,
|
||||
isImportModalVisible: false,
|
||||
isNewCalendarIdValid: null,
|
||||
loading: true,
|
||||
jobIds: [],
|
||||
jobIdOptions: [],
|
||||
groupIds: [],
|
||||
groupIdOptions: [],
|
||||
calendars: [],
|
||||
formCalendarId: '',
|
||||
description: '',
|
||||
selectedJobOptions: [],
|
||||
selectedGroupOptions: [],
|
||||
events: [],
|
||||
saving: false,
|
||||
selectedCalendar: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.formSetup();
|
||||
}
|
||||
|
||||
async formSetup() {
|
||||
try {
|
||||
const { jobIds, groupIds, calendars } = await getCalendarSettingsData();
|
||||
|
||||
const jobIdOptions = jobIds.map((jobId) => ({ label: jobId }));
|
||||
const groupIdOptions = groupIds.map((groupId) => ({ label: groupId }));
|
||||
|
||||
const selectedJobOptions = [];
|
||||
const selectedGroupOptions = [];
|
||||
let eventsList = [];
|
||||
let selectedCalendar;
|
||||
let formCalendarId = '';
|
||||
|
||||
// Editing existing calendar.
|
||||
if (this.props.calendarId !== undefined) {
|
||||
selectedCalendar = calendars.find((cal) => cal.calendar_id === this.props.calendarId);
|
||||
|
||||
if (selectedCalendar) {
|
||||
formCalendarId = selectedCalendar.calendar_id;
|
||||
eventsList = selectedCalendar.events;
|
||||
|
||||
selectedCalendar.job_ids.forEach(id => {
|
||||
if (jobIds.find((jobId) => jobId === id)) {
|
||||
selectedJobOptions.push({ label: id });
|
||||
} else if (groupIds.find((groupId) => groupId === id)) {
|
||||
selectedGroupOptions.push({ label: id });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
events: eventsList,
|
||||
formCalendarId,
|
||||
jobIds,
|
||||
jobIdOptions,
|
||||
groupIds,
|
||||
groupIdOptions,
|
||||
calendars,
|
||||
loading: false,
|
||||
selectedJobOptions,
|
||||
selectedGroupOptions,
|
||||
selectedCalendar
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.setState({ loading: false });
|
||||
toastNotifications.addDanger('An error occurred loading calendar form data. Try refreshing the page.');
|
||||
}
|
||||
}
|
||||
|
||||
onCreate = async () => {
|
||||
const calendar = this.setUpCalendarForApi();
|
||||
this.setState({ saving: true });
|
||||
|
||||
try {
|
||||
await ml.addCalendar(calendar);
|
||||
window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
|
||||
} catch (error) {
|
||||
console.log('Error saving calendar', error);
|
||||
this.setState({ saving: false });
|
||||
toastNotifications.addDanger(`An error occurred creating calendar ${calendar.calendarId}`);
|
||||
}
|
||||
}
|
||||
|
||||
onEdit = async () => {
|
||||
const calendar = this.setUpCalendarForApi();
|
||||
this.setState({ saving: true });
|
||||
|
||||
try {
|
||||
await ml.updateCalendar(calendar);
|
||||
window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
|
||||
} catch (error) {
|
||||
console.log('Error saving calendar', error);
|
||||
this.setState({ saving: false });
|
||||
toastNotifications.addDanger(`An error occurred saving calendar ${calendar.calendarId}. Try refreshing the page.`);
|
||||
}
|
||||
}
|
||||
|
||||
setUpCalendarForApi = () => {
|
||||
const {
|
||||
formCalendarId,
|
||||
description,
|
||||
events,
|
||||
selectedGroupOptions,
|
||||
selectedJobOptions,
|
||||
} = this.state;
|
||||
|
||||
const jobIds = selectedJobOptions.map((option) => option.label);
|
||||
const groupIds = selectedGroupOptions.map((option) => option.label);
|
||||
|
||||
// Reduce events to fields expected by api
|
||||
const eventsToSave = events.map((event) => ({
|
||||
description: event.description,
|
||||
start_time: event.start_time,
|
||||
end_time: event.end_time
|
||||
}));
|
||||
|
||||
// set up calendar
|
||||
const calendar = {
|
||||
calendarId: formCalendarId,
|
||||
description,
|
||||
events: eventsToSave,
|
||||
job_ids: [...jobIds, ...groupIds]
|
||||
};
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
onCreateGroupOption = (newGroup) => {
|
||||
const newOption = {
|
||||
label: newGroup,
|
||||
};
|
||||
// Select the option.
|
||||
this.setState(prevState => ({
|
||||
selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption),
|
||||
}));
|
||||
};
|
||||
|
||||
onJobSelection = (selectedJobOptions) => {
|
||||
this.setState({
|
||||
selectedJobOptions,
|
||||
});
|
||||
};
|
||||
|
||||
onGroupSelection = (selectedGroupOptions) => {
|
||||
this.setState({
|
||||
selectedGroupOptions,
|
||||
});
|
||||
};
|
||||
|
||||
onCalendarIdChange = (e) => {
|
||||
const isValid = validateCalendarId(e.target.value);
|
||||
|
||||
this.setState({
|
||||
formCalendarId: e.target.value,
|
||||
isNewCalendarIdValid: isValid
|
||||
});
|
||||
};
|
||||
|
||||
onDescriptionChange = (e) => {
|
||||
this.setState({
|
||||
description: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
showImportModal = () => {
|
||||
this.setState(prevState => ({
|
||||
isImportModalVisible: !prevState.isImportModalVisible,
|
||||
}));
|
||||
}
|
||||
|
||||
closeImportModal = () => {
|
||||
this.setState({
|
||||
isImportModalVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
onEventDelete = (eventId) => {
|
||||
this.setState(prevState => ({
|
||||
events: prevState.events.filter(event => event.event_id !== eventId)
|
||||
}));
|
||||
}
|
||||
|
||||
closeNewEventModal = () => {
|
||||
this.setState({ isNewEventModalVisible: false });
|
||||
}
|
||||
|
||||
showNewEventModal = () => {
|
||||
this.setState({ isNewEventModalVisible: true });
|
||||
}
|
||||
|
||||
addEvent = (event) => {
|
||||
this.setState(prevState => ({
|
||||
events: [...prevState.events, event],
|
||||
isNewEventModalVisible: false
|
||||
}));
|
||||
}
|
||||
|
||||
addImportedEvents = (events) => {
|
||||
this.setState(prevState => ({
|
||||
events: [...prevState.events, ...events],
|
||||
isImportModalVisible: false
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
events,
|
||||
isNewEventModalVisible,
|
||||
isImportModalVisible,
|
||||
isNewCalendarIdValid,
|
||||
formCalendarId,
|
||||
description,
|
||||
groupIdOptions,
|
||||
jobIdOptions,
|
||||
saving,
|
||||
selectedCalendar,
|
||||
selectedJobOptions,
|
||||
selectedGroupOptions
|
||||
} = this.state;
|
||||
|
||||
let modal = '';
|
||||
|
||||
if (isNewEventModalVisible) {
|
||||
modal = (
|
||||
<EuiOverlayMask>
|
||||
<NewEventModal
|
||||
addEvent={this.addEvent}
|
||||
closeModal={this.closeNewEventModal}
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
} else if (isImportModalVisible) {
|
||||
modal = (
|
||||
<EuiOverlayMask>
|
||||
<ImportModal
|
||||
addImportedEvents={this.addImportedEvents}
|
||||
closeImportModal={this.closeImportModal}
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPage className="mlCalendarEditForm">
|
||||
<EuiPageContent
|
||||
className="mlCalendarEditForm__content"
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
>
|
||||
<CalendarForm
|
||||
calendarId={selectedCalendar ? selectedCalendar.calendar_id : formCalendarId}
|
||||
description={selectedCalendar ? selectedCalendar.description : description}
|
||||
eventsList={events}
|
||||
groupIds={groupIdOptions}
|
||||
isEdit={selectedCalendar !== undefined}
|
||||
isNewCalendarIdValid={(selectedCalendar || isNewCalendarIdValid === null) ? true : isNewCalendarIdValid}
|
||||
jobIds={jobIdOptions}
|
||||
onCalendarIdChange={this.onCalendarIdChange}
|
||||
onCreate={this.onCreate}
|
||||
onDescriptionChange={this.onDescriptionChange}
|
||||
onEdit={this.onEdit}
|
||||
onEventDelete={this.onEventDelete}
|
||||
onGroupSelection={this.onGroupSelection}
|
||||
showImportModal={this.showImportModal}
|
||||
onJobSelection={this.onJobSelection}
|
||||
saving={saving}
|
||||
selectedGroupOptions={selectedGroupOptions}
|
||||
selectedJobOptions={selectedJobOptions}
|
||||
onCreateGroupOption={this.onCreateGroupOption}
|
||||
showNewEventModal={this.showNewEventModal}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
{modal}
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewCalendar.propTypes = {
|
||||
calendarId: PropTypes.string,
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
jest.mock('../../../privilege/check_privilege', () => ({
|
||||
checkPermission: () => true
|
||||
}));
|
||||
jest.mock('../../../license/check_license', () => ({
|
||||
hasLicenseExpired: () => false
|
||||
}));
|
||||
jest.mock('../../../privilege/get_privileges', () => ({
|
||||
getPrivileges: () => {}
|
||||
}));
|
||||
jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
|
||||
mlNodesAvailable: () => true
|
||||
}));
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
jest.mock('../../../services/ml_api_service', () => ({
|
||||
ml: {
|
||||
calendars: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
jobs: {
|
||||
jobsSummary: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
groups: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
},
|
||||
}
|
||||
}));
|
||||
jest.mock('./utils', () => ({
|
||||
getCalendarSettingsData: jest.fn().mockImplementation(() => new Promise((resolve) => {
|
||||
resolve({
|
||||
jobIds: ['test-job-one', 'test-job-2'],
|
||||
groupIds: ['test-group-one', 'test-group-two'],
|
||||
calendars: []
|
||||
});
|
||||
})),
|
||||
}));
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { NewCalendar } from './new_calendar';
|
||||
|
||||
describe('NewCalendar', () => {
|
||||
|
||||
test('Renders new calendar form', () => {
|
||||
const wrapper = shallow(
|
||||
<NewCalendar />
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Import modal shown on Import Events button click', () => {
|
||||
const wrapper = mount(
|
||||
<NewCalendar />
|
||||
);
|
||||
|
||||
const importButton = wrapper.find('[data-testid="ml_import_events"]');
|
||||
const button = importButton.find('EuiButton');
|
||||
button.simulate('click');
|
||||
|
||||
expect(wrapper.state('isImportModalVisible')).toBe(true);
|
||||
});
|
||||
|
||||
test('New event modal shown on New event button click', () => {
|
||||
const wrapper = mount(
|
||||
<NewCalendar />
|
||||
);
|
||||
|
||||
const importButton = wrapper.find('[data-testid="ml_new_event"]');
|
||||
const button = importButton.find('EuiButton');
|
||||
button.simulate('click');
|
||||
|
||||
expect(wrapper.state('isNewEventModalVisible')).toBe(true);
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { NewEventModal } from './new_event_modal';
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import React, {
|
||||
Component,
|
||||
Fragment
|
||||
} from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDatePicker,
|
||||
EuiDatePickerRange,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { TIME_FORMAT } from '../events_table/';
|
||||
import { generateTempId } from '../utils';
|
||||
|
||||
const VALID_DATE_STRING_LENGTH = 19;
|
||||
|
||||
export class NewEventModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const startDate = moment().startOf('day');
|
||||
const endDate = moment().startOf('day').add(1, 'days');
|
||||
|
||||
this.state = {
|
||||
startDate,
|
||||
endDate,
|
||||
description: '',
|
||||
startDateString: startDate.format(TIME_FORMAT),
|
||||
endDateString: endDate.format(TIME_FORMAT)
|
||||
};
|
||||
}
|
||||
|
||||
onDescriptionChange = (e) => {
|
||||
this.setState({
|
||||
description: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
handleAddEvent = () => {
|
||||
const { description, startDate, endDate } = this.state;
|
||||
// Temp reference to unsaved events to allow removal from table
|
||||
const tempId = generateTempId();
|
||||
|
||||
const event = {
|
||||
description,
|
||||
start_time: startDate.valueOf(),
|
||||
end_time: endDate.valueOf(),
|
||||
event_id: tempId
|
||||
};
|
||||
|
||||
this.props.addEvent(event);
|
||||
}
|
||||
|
||||
handleChangeStart = (date) => {
|
||||
let start = null;
|
||||
let end = this.state.endDate;
|
||||
|
||||
const startMoment = moment(date);
|
||||
const endMoment = moment(date);
|
||||
|
||||
start = startMoment.startOf('day');
|
||||
|
||||
if (start > end) {
|
||||
end = endMoment.startOf('day').add(1, 'days');
|
||||
}
|
||||
this.setState({
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
startDateString: start.format(TIME_FORMAT),
|
||||
endDateString: end.format(TIME_FORMAT)
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeEnd = (date) => {
|
||||
let start = this.state.startDate;
|
||||
let end = null;
|
||||
|
||||
const startMoment = moment(date);
|
||||
const endMoment = moment(date);
|
||||
|
||||
end = endMoment.startOf('day');
|
||||
|
||||
if (start > end) {
|
||||
start = startMoment.startOf('day').subtract(1, 'days');
|
||||
}
|
||||
this.setState({
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
startDateString: start.format(TIME_FORMAT),
|
||||
endDateString: end.format(TIME_FORMAT)
|
||||
});
|
||||
}
|
||||
|
||||
handleTimeStartChange = (event) => {
|
||||
const dateString = event.target.value;
|
||||
let isValidDate = false;
|
||||
|
||||
if (dateString.length === VALID_DATE_STRING_LENGTH) {
|
||||
isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
|
||||
} else {
|
||||
this.setState({
|
||||
startDateString: dateString,
|
||||
});
|
||||
}
|
||||
|
||||
if (isValidDate) {
|
||||
this.setState({
|
||||
startDateString: dateString,
|
||||
startDate: moment(dateString)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleTimeEndChange = (event) => {
|
||||
const dateString = event.target.value;
|
||||
let isValidDate = false;
|
||||
|
||||
if (dateString.length === VALID_DATE_STRING_LENGTH) {
|
||||
isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
|
||||
} else {
|
||||
this.setState({
|
||||
endDateString: dateString,
|
||||
});
|
||||
}
|
||||
|
||||
if (isValidDate) {
|
||||
this.setState({
|
||||
endDateString: dateString,
|
||||
endDate: moment(dateString)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderRangedDatePicker = () => {
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
startDateString,
|
||||
endDateString,
|
||||
} = this.state;
|
||||
|
||||
const timeInputs = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="From:" helpText={TIME_FORMAT}>
|
||||
<EuiFieldText
|
||||
name="startTime"
|
||||
onChange={this.handleTimeStartChange}
|
||||
placeholder={TIME_FORMAT}
|
||||
value={startDateString}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="To:" helpText={TIME_FORMAT}>
|
||||
<EuiFieldText
|
||||
name="endTime"
|
||||
onChange={this.handleTimeEndChange}
|
||||
placeholder={TIME_FORMAT}
|
||||
value={endDateString}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
{timeInputs}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiDatePickerRange
|
||||
fullWidth
|
||||
iconType={false}
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
inline
|
||||
selected={startDate}
|
||||
onChange={this.handleChangeStart}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isInvalid={startDate > endDate}
|
||||
aria-label="Start date"
|
||||
timeFormat={TIME_FORMAT}
|
||||
dateFormat={TIME_FORMAT}
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
fullWidth
|
||||
inline
|
||||
selected={endDate}
|
||||
onChange={this.handleChangeEnd}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isInvalid={startDate > endDate}
|
||||
aria-label="End date"
|
||||
timeFormat={TIME_FORMAT}
|
||||
dateFormat={TIME_FORMAT}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { closeModal } = this.props;
|
||||
const { description } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiModal
|
||||
onClose={closeModal}
|
||||
initialFocus="[name=description]"
|
||||
maxWidth={false}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle >
|
||||
Create new event
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label="Description"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
name="description"
|
||||
onChange={this.onDescriptionChange}
|
||||
isInvalid={!description}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{this.renderRangedDatePicker()}
|
||||
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
onClick={this.handleAddEvent}
|
||||
fill
|
||||
disabled={!description}
|
||||
>
|
||||
Add
|
||||
</EuiButton>
|
||||
<EuiButtonEmpty
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewEventModal.propTypes = {
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
addEvent: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { NewEventModal } from './new_event_modal';
|
||||
import moment from 'moment';
|
||||
|
||||
const testProps = {
|
||||
closeModal: jest.fn(),
|
||||
addEvent: jest.fn(),
|
||||
};
|
||||
|
||||
const stateTimestamps = {
|
||||
startDate: 1544508000000,
|
||||
endDate: 1544594400000
|
||||
};
|
||||
|
||||
describe('NewEventModal', () => {
|
||||
|
||||
it('Add button disabled if description empty', () => {
|
||||
const wrapper = shallow(
|
||||
<NewEventModal {...testProps} />
|
||||
);
|
||||
|
||||
const addButton = wrapper.find('EuiButton').first();
|
||||
expect(addButton.prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('if endDate is less than startDate should set startDate one day before endDate', () => {
|
||||
const wrapper = shallow(<NewEventModal {...testProps} />);
|
||||
const instance = wrapper.instance();
|
||||
instance.setState({
|
||||
startDate: moment(stateTimestamps.startDate),
|
||||
endDate: moment(stateTimestamps.endDate)
|
||||
});
|
||||
// set to Dec 11, 2018 and Dec 12, 2018
|
||||
const startMoment = moment(stateTimestamps.startDate);
|
||||
const endMoment = moment(stateTimestamps.endDate);
|
||||
// make startMoment greater than current end Date
|
||||
startMoment.startOf('day').add(3, 'days');
|
||||
// trigger handleChangeStart directly with startMoment
|
||||
instance.handleChangeStart(startMoment);
|
||||
// add 3 days to endMoment as it will be adjusted to be one day after startDate
|
||||
const expected = endMoment.startOf('day').add(3, 'days').format();
|
||||
|
||||
expect(wrapper.state('endDate').format()).toBe(expected);
|
||||
});
|
||||
|
||||
it('if startDate is greater than endDate should set endDate one day after startDate', () => {
|
||||
const wrapper = shallow(<NewEventModal {...testProps} />);
|
||||
const instance = wrapper.instance();
|
||||
instance.setState({
|
||||
startDate: moment(stateTimestamps.startDate),
|
||||
endDate: moment(stateTimestamps.endDate)
|
||||
});
|
||||
|
||||
// set to Dec 11, 2018 and Dec 12, 2018
|
||||
const startMoment = moment(stateTimestamps.startDate);
|
||||
const endMoment = moment(stateTimestamps.endDate);
|
||||
// make endMoment less than current start Date
|
||||
endMoment.startOf('day').subtract(3, 'days');
|
||||
// trigger handleChangeStart directly with endMoment
|
||||
instance.handleChangeStart(endMoment);
|
||||
// subtract 3 days from startDate as it will be adjusted to be one day before endDate
|
||||
const expected = startMoment.startOf('day').subtract(2, 'days').format();
|
||||
|
||||
expect(wrapper.state('startDate').format()).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
87
x-pack/plugins/ml/public/settings/calendars/edit/utils.js
Normal file
87
x-pack/plugins/ml/public/settings/calendars/edit/utils.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { isJobIdValid } from '../../../../common/util/job_utils';
|
||||
|
||||
|
||||
function getJobIds() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ml.jobs.jobsSummary()
|
||||
.then((resp) => {
|
||||
resolve(resp.map((job) => job.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = `Error fetching job summaries: ${err}`;
|
||||
console.log(errorMessage);
|
||||
reject(errorMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getGroupIds() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ml.jobs.groups()
|
||||
.then((resp) => {
|
||||
resolve(resp.map((group) => group.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = `Error loading groups: ${err}`;
|
||||
console.log(errorMessage);
|
||||
reject(errorMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCalendars() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ml.calendars()
|
||||
.then((resp) => {
|
||||
resolve(resp);
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = `Error loading calendars: ${err}`;
|
||||
console.log(errorMessage);
|
||||
reject(errorMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getCalendarSettingsData() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]);
|
||||
|
||||
const formattedData = {
|
||||
jobIds: data[0],
|
||||
groupIds: data[1],
|
||||
calendars: data[2]
|
||||
};
|
||||
resolve(formattedData);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function validateCalendarId(calendarId) {
|
||||
let valid = true;
|
||||
|
||||
if (calendarId === '' || calendarId === undefined) {
|
||||
valid = false;
|
||||
} else if (isJobIdValid(calendarId) === false) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
export function generateTempId() {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
10
x-pack/plugins/ml/public/settings/calendars/index.js
Normal file
10
x-pack/plugins/ml/public/settings/calendars/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './list';
|
||||
import './edit';
|
|
@ -0,0 +1,65 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CalendarsList Renders calendar list with calendars 1`] = `
|
||||
<EuiPage
|
||||
className="mlCalendarList"
|
||||
restrictWidth={false}
|
||||
>
|
||||
<EuiPageContent
|
||||
className="mlCalendarList__content"
|
||||
horizontalPosition="center"
|
||||
panelPaddingSize="l"
|
||||
verticalPosition="center"
|
||||
>
|
||||
<CalendarsListTable
|
||||
calendarsList={
|
||||
Array [
|
||||
Object {
|
||||
"calendar_id": "farequote-calendar",
|
||||
"description": "test ",
|
||||
"events": Array [
|
||||
Object {
|
||||
"calendar_id": "farequote-calendar",
|
||||
"description": "Downtime feb 9 2017 10:10 to 10:30",
|
||||
"end_time": 1486657800000,
|
||||
"event_id": "Ee-YgGcBxHgQWEhCO_xj",
|
||||
"start_time": 1486656600000,
|
||||
},
|
||||
],
|
||||
"events_length": "1 event",
|
||||
"job_ids": Array [
|
||||
"farequote",
|
||||
],
|
||||
"job_ids_string": "farequote",
|
||||
},
|
||||
Object {
|
||||
"calendar_id": "this-is-a-new-calendar",
|
||||
"description": "new calendar",
|
||||
"events": Array [
|
||||
Object {
|
||||
"calendar_id": "this-is-a-new-calendar",
|
||||
"description": "New event!",
|
||||
"end_time": 1544162400000,
|
||||
"event_id": "ehWKhGcBqHkXuWNrIrSV",
|
||||
"start_time": 1544076000000,
|
||||
},
|
||||
],
|
||||
"events_length": "1 event",
|
||||
"job_ids": Array [
|
||||
"test",
|
||||
],
|
||||
"job_ids_string": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
canCreateCalendar={true}
|
||||
canDeleteCalendar={true}
|
||||
itemsSelected={false}
|
||||
loading={false}
|
||||
mlNodesAvailable={true}
|
||||
onDeleteClick={[Function]}
|
||||
setSelectedCalendarList={[Function]}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
`;
|
|
@ -0,0 +1 @@
|
|||
@import 'list';
|
|
@ -0,0 +1,9 @@
|
|||
.mlCalendarList {
|
||||
|
||||
.mlCalendarList__content {
|
||||
max-width: map-get($euiBreakpoints, 'xl');
|
||||
margin-top: $euiSize;
|
||||
margin-bottom: $euiSize;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React, {
|
||||
Component
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EuiPage,
|
||||
EuiPageContent,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CalendarsListTable } from './table/';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { checkPermission } from '../../../privilege/check_privilege';
|
||||
import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
|
||||
import { deleteCalendars } from './delete_calendars';
|
||||
|
||||
export class CalendarsList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
calendars: [],
|
||||
isDestroyModalVisible: false,
|
||||
calendarId: null,
|
||||
selectedForDeletion: [],
|
||||
canCreateCalendar: checkPermission('canCreateCalendar'),
|
||||
canDeleteCalendar: checkPermission('canDeleteCalendar'),
|
||||
nodesAvailable: mlNodesAvailable()
|
||||
};
|
||||
}
|
||||
|
||||
loadCalendars = async () => {
|
||||
try {
|
||||
const calendars = await ml.calendars();
|
||||
|
||||
this.setState({
|
||||
calendars,
|
||||
loading: false,
|
||||
isDestroyModalVisible: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.setState({ loading: false });
|
||||
toastNotifications.addDanger('An error occurred loading the list of calendars.');
|
||||
}
|
||||
}
|
||||
|
||||
closeDestroyModal = () => {
|
||||
this.setState({ isDestroyModalVisible: false, calendarId: null });
|
||||
}
|
||||
|
||||
showDestroyModal = () => {
|
||||
this.setState({ isDestroyModalVisible: true });
|
||||
}
|
||||
|
||||
setSelectedCalendarList = (selectedCalendars) => {
|
||||
this.setState({ selectedForDeletion: selectedCalendars });
|
||||
}
|
||||
|
||||
deleteCalendars = () => {
|
||||
const { selectedForDeletion } = this.state;
|
||||
|
||||
this.closeDestroyModal();
|
||||
deleteCalendars(selectedForDeletion, this.loadCalendars);
|
||||
}
|
||||
|
||||
addRequiredFieldsToList = (calendarsList = []) => {
|
||||
for (let i = 0; i < calendarsList.length; i++) {
|
||||
const eventLength = calendarsList[i].events.length;
|
||||
calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', ');
|
||||
calendarsList[i].events_length = `${eventLength} ${eventLength === 1 ? 'event' : 'events'}`;
|
||||
}
|
||||
|
||||
return calendarsList;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadCalendars();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
calendars,
|
||||
selectedForDeletion,
|
||||
loading,
|
||||
canCreateCalendar,
|
||||
canDeleteCalendar,
|
||||
nodesAvailable
|
||||
} = this.state;
|
||||
let destroyModal = '';
|
||||
|
||||
if (this.state.isDestroyModalVisible) {
|
||||
destroyModal = (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title="Delete calendar"
|
||||
onCancel={this.closeDestroyModal}
|
||||
onConfirm={this.deleteCalendars}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
>
|
||||
<p>
|
||||
{
|
||||
`Delete ${selectedForDeletion.length === 1 ? 'this' : 'these'}
|
||||
calendar${selectedForDeletion.length === 1 ? '' : 's'}?
|
||||
${selectedForDeletion.map((c) => c.calendar_id).join(', ')}`
|
||||
}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPage className="mlCalendarList">
|
||||
<EuiPageContent
|
||||
className="mlCalendarList__content"
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
>
|
||||
<CalendarsListTable
|
||||
loading={loading}
|
||||
calendarsList={this.addRequiredFieldsToList(calendars)}
|
||||
onDeleteClick={this.showDestroyModal}
|
||||
canCreateCalendar={canCreateCalendar}
|
||||
canDeleteCalendar={canDeleteCalendar}
|
||||
mlNodesAvailable={nodesAvailable}
|
||||
setSelectedCalendarList={this.setSelectedCalendarList}
|
||||
itemsSelected={selectedForDeletion.length > 0}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
{destroyModal}
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
jest.mock('../../../privilege/check_privilege', () => ({
|
||||
checkPermission: () => true
|
||||
}));
|
||||
jest.mock('../../../license/check_license', () => ({
|
||||
hasLicenseExpired: () => false
|
||||
}));
|
||||
jest.mock('../../../privilege/get_privileges', () => ({
|
||||
getPrivileges: () => {}
|
||||
}));
|
||||
jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
|
||||
mlNodesAvailable: () => true
|
||||
}));
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
jest.mock('../../../services/ml_api_service', () => ({
|
||||
ml: {
|
||||
calendars: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
delete: jest.fn(),
|
||||
}
|
||||
}));
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
import { CalendarsList } from './calendars_list';
|
||||
|
||||
const testingState = {
|
||||
loading: false,
|
||||
calendars: [
|
||||
{
|
||||
'calendar_id': 'farequote-calendar',
|
||||
'job_ids': ['farequote'],
|
||||
'description': 'test ',
|
||||
'events': [{
|
||||
'description': 'Downtime feb 9 2017 10:10 to 10:30',
|
||||
'start_time': 1486656600000,
|
||||
'end_time': 1486657800000,
|
||||
'calendar_id': 'farequote-calendar',
|
||||
'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
|
||||
}]
|
||||
},
|
||||
{
|
||||
'calendar_id': 'this-is-a-new-calendar',
|
||||
'job_ids': ['test'],
|
||||
'description': 'new calendar',
|
||||
'events': [{
|
||||
'description': 'New event!',
|
||||
'start_time': 1544076000000,
|
||||
'end_time': 1544162400000,
|
||||
'calendar_id': 'this-is-a-new-calendar',
|
||||
'event_id': 'ehWKhGcBqHkXuWNrIrSV'
|
||||
}]
|
||||
}],
|
||||
isDestroyModalVisible: false,
|
||||
calendarId: null,
|
||||
selectedForDeletion: [],
|
||||
canCreateCalendar: true,
|
||||
canDeleteCalendar: true,
|
||||
nodesAvailable: true,
|
||||
};
|
||||
|
||||
describe('CalendarsList', () => {
|
||||
|
||||
test('loads calendars on mount', () => {
|
||||
ml.calendars = jest.fn();
|
||||
shallow(
|
||||
<CalendarsList />
|
||||
);
|
||||
|
||||
expect(ml.calendars).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Renders calendar list with calendars', () => {
|
||||
const wrapper = shallow(
|
||||
<CalendarsList />
|
||||
);
|
||||
|
||||
wrapper.instance().setState(testingState);
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Sets selected calendars list on checkbox change', () => {
|
||||
const wrapper = mount(
|
||||
<CalendarsList />
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
const spy = jest.spyOn(instance, 'setSelectedCalendarList');
|
||||
instance.setState(testingState);
|
||||
wrapper.update();
|
||||
|
||||
const checkbox = wrapper.find('input[type="checkbox"]').first();
|
||||
checkbox.simulate('change');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
|
||||
export async function deleteCalendars(calendarsToDelete, callback) {
|
||||
if (calendarsToDelete === undefined || calendarsToDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete each of the specified calendars in turn, waiting for each response
|
||||
// before deleting the next to minimize load on the cluster.
|
||||
const messageId = `${(calendarsToDelete.length > 1) ?
|
||||
`${calendarsToDelete.length} calendars` : calendarsToDelete[0].calendar_id}`;
|
||||
toastNotifications.add(`Deleting ${messageId}`);
|
||||
|
||||
for(const calendar of calendarsToDelete) {
|
||||
const calendarId = calendar.calendar_id;
|
||||
try {
|
||||
await ml.deleteCalendar({ calendarId });
|
||||
} catch (error) {
|
||||
console.log('Error deleting calendar:', error);
|
||||
let errorMessage = `An error occurred deleting calendar ${calendar.calendar_id}`;
|
||||
if (error.message) {
|
||||
errorMessage += ` : ${error.message}`;
|
||||
}
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess(`${messageId} deleted`);
|
||||
callback();
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { checkFullLicense } from '../../../license/check_license';
|
||||
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
|
||||
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
|
||||
import { initPromise } from '../../../util/promise';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
||||
const template = `
|
||||
<ml-nav-menu name="settings" />
|
||||
<div class="mlCalendarManagement">
|
||||
<ml-calendars-list />
|
||||
</div>
|
||||
`;
|
||||
|
||||
uiRoutes
|
||||
.when('/settings/calendars_list', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
mlNodeCount: getMlNodeCount,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
import { CalendarsList } from './calendars_list';
|
||||
|
||||
module.directive('mlCalendarsList', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
scope: {},
|
||||
link: function (scope, element) {
|
||||
ReactDOM.render(
|
||||
React.createElement(CalendarsList),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './directive';
|
|
@ -0,0 +1,110 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CalendarsListTable renders the table with all calendars 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "calendar_id",
|
||||
"name": "ID",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
"truncateText": true,
|
||||
},
|
||||
Object {
|
||||
"field": "job_ids_string",
|
||||
"name": "Jobs",
|
||||
"sortable": true,
|
||||
"truncateText": true,
|
||||
},
|
||||
Object {
|
||||
"field": "events_length",
|
||||
"name": "Events",
|
||||
"sortable": true,
|
||||
},
|
||||
]
|
||||
}
|
||||
isSelectable={true}
|
||||
itemId="calendar_id"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"calendar_id": "farequote-calendar",
|
||||
"description": "test ",
|
||||
"events": Array [],
|
||||
"job_ids": Array [
|
||||
"farequote",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"calendar_id": "this-is-a-new-calendar",
|
||||
"description": "new calendar",
|
||||
"events": Array [],
|
||||
"job_ids": Array [
|
||||
"test",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={false}
|
||||
pagination={
|
||||
Object {
|
||||
"initialPageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
],
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"box": Object {
|
||||
"incremental": true,
|
||||
},
|
||||
"filters": Array [],
|
||||
"toolsRight": Array [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-testid="new_calendar_button"
|
||||
fill={false}
|
||||
href="undefined/app/ml#/settings/calendars_list/new_calendar"
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
New
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
color="danger"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="trash"
|
||||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</EuiButton>,
|
||||
],
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "calendar_id",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { CalendarsListTable } from './table';
|
132
x-pack/plugins/ml/public/settings/calendars/list/table/table.js
Normal file
132
x-pack/plugins/ml/public/settings/calendars/list/table/table.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
EuiInMemoryTable,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
|
||||
export function CalendarsListTable({
|
||||
calendarsList,
|
||||
onDeleteClick,
|
||||
setSelectedCalendarList,
|
||||
loading,
|
||||
canCreateCalendar,
|
||||
canDeleteCalendar,
|
||||
mlNodesAvailable,
|
||||
itemsSelected
|
||||
}) {
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'calendar_id',
|
||||
direction: 'asc',
|
||||
}
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20]
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'calendar_id',
|
||||
name: 'ID',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (id) => (
|
||||
<EuiLink
|
||||
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/edit_calendar/${id}`}
|
||||
>
|
||||
{id}
|
||||
</EuiLink>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'job_ids_string',
|
||||
name: 'Jobs',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'events_length',
|
||||
name: 'Events',
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
|
||||
const tableSelection = {
|
||||
onSelectionChange: (selection) => setSelectedCalendarList(selection)
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsRight: [
|
||||
(
|
||||
<EuiButton
|
||||
size="s"
|
||||
data-testid="new_calendar_button"
|
||||
key="new_calendar_button"
|
||||
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/new_calendar`}
|
||||
isDisabled={(canCreateCalendar === false || mlNodesAvailable === false)}
|
||||
>
|
||||
New
|
||||
</EuiButton>
|
||||
),
|
||||
(
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={onDeleteClick}
|
||||
isDisabled={(canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false)}
|
||||
>
|
||||
Delete
|
||||
</EuiButton>
|
||||
)
|
||||
],
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: []
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiInMemoryTable
|
||||
items={calendarsList}
|
||||
itemId="calendar_id"
|
||||
columns={columns}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
loading={loading}
|
||||
selection={tableSelection}
|
||||
isSelectable={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
CalendarsListTable.propTypes = {
|
||||
calendarsList: PropTypes.array.isRequired,
|
||||
onDeleteClick: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
canCreateCalendar: PropTypes.bool.isRequired,
|
||||
canDeleteCalendar: PropTypes.bool.isRequired,
|
||||
mlNodesAvailable: PropTypes.bool.isRequired,
|
||||
setSelectedCalendarList: PropTypes.func.isRequired,
|
||||
itemsSelected: PropTypes.bool.isRequired,
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { CalendarsListTable } from './table';
|
||||
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: jest.fn()
|
||||
}));
|
||||
|
||||
const calendars = [
|
||||
{
|
||||
'calendar_id': 'farequote-calendar',
|
||||
'job_ids': ['farequote'],
|
||||
'description': 'test ',
|
||||
'events': [] },
|
||||
{
|
||||
'calendar_id': 'this-is-a-new-calendar',
|
||||
'job_ids': ['test'],
|
||||
'description': 'new calendar',
|
||||
'events': [] }];
|
||||
|
||||
const props = {
|
||||
calendarsList: calendars,
|
||||
canCreateCalendar: true,
|
||||
canDeleteCalendar: true,
|
||||
itemsSelected: false,
|
||||
loading: false,
|
||||
mlNodesAvailable: true,
|
||||
onDeleteClick: () => { },
|
||||
setSelectedCalendarList: () => { }
|
||||
};
|
||||
|
||||
describe('CalendarsListTable', () => {
|
||||
|
||||
test('renders the table with all calendars', () => {
|
||||
const wrapper = shallow(
|
||||
<CalendarsListTable {...props} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('New button enabled if permission available', () => {
|
||||
const wrapper = mount(
|
||||
<CalendarsListTable {...props} />
|
||||
);
|
||||
|
||||
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
|
||||
const button = buttons.find('EuiButton');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(false);
|
||||
});
|
||||
|
||||
test('New button disabled if no permission available', () => {
|
||||
const disableProps = {
|
||||
...props,
|
||||
canCreateCalendar: false
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<CalendarsListTable {...disableProps} />
|
||||
);
|
||||
|
||||
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
|
||||
const button = buttons.find('EuiButton');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
|
||||
test('New button disabled if no ML nodes available', () => {
|
||||
const disableProps = {
|
||||
...props,
|
||||
mlNodesAvailable: false
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<CalendarsListTable {...disableProps} />
|
||||
);
|
||||
|
||||
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
|
||||
const button = buttons.find('EuiButton');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
});
|
|
@ -7,5 +7,5 @@
|
|||
|
||||
|
||||
import './settings_controller';
|
||||
import './scheduled_events';
|
||||
import './calendars';
|
||||
import './filter_lists';
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
|
||||
describe('ML - Calendars List Controller', () => {
|
||||
xdescribe('ML - Calendars List Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import expect from 'expect.js';
|
|||
|
||||
const mockModalInstance = { close: function () { }, dismiss: function () { } };
|
||||
|
||||
describe('ML - Import Events Modal Controller', () => {
|
||||
xdescribe('ML - Import Events Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import expect from 'expect.js';
|
|||
|
||||
const mockModalInstance = { close: function () { }, dismiss: function () { } };
|
||||
|
||||
describe('ML - New Event Modal Controller', () => {
|
||||
xdescribe('ML - New Event Modal Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
|
||||
describe('ML - Create Calendar Controller', () => {
|
||||
xdescribe('ML - Create Calendar Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue