From 9b3384387e6aa03570eb682d940ca095105700a2 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Tue, 27 Aug 2019 14:25:48 -0700
Subject: [PATCH] [SR] SLM create and edit policies (#43390)
* add buttons and links to create/edit policy
* set up add policy form
* start create policy form, including loading/error states and redirect for repository select field. add inline option to SectionLoading. add actions prop to SectionError
* add snapshot name field
* Change page title upon app navigation, improve breadcrumbs
* Add on cancel to policy form, reorder fields
* Add simple cron field
* First pass at create/edit policy functionality
* Adjust permissions for SLM tab
* Adjust no snapshots prompt based on if policies exist or not
* Add selectable indices to policy form
* Move cron editor from rollup jobs to ES UI shared folder
* Used shared cron editor for slm policy create/edit
* Adjust copies; add duplicate schedule warning callout
* Surface in progress information
* Fix doc link for 7.x
* Fix rollup tests
* Copy edits from review
* Add ES endpoint to request review
* Remove unused imports
* Fix i18n by cleaning up typo'd text
* Remove unused import
* Fix permissions and i18n
* Revert change to Logistics copy
* Fix bugs and PR feedback
* Add cancel button to form and add comment for list
* Adjust timeout comment
* Fix bug with list of indices in detail panel when clicking through table
* Add comment about EUI bug
---
.i18nrc.json | 3 +-
.../components/cron_editor/cron_daily.js | 29 +-
.../components/cron_editor/cron_editor.js | 27 +-
.../components/cron_editor/cron_hourly.js | 71 +++
.../components/cron_editor/cron_monthly.js | 37 +-
.../components/cron_editor/cron_weekly.js | 37 +-
.../components/cron_editor/cron_yearly.js | 45 +-
.../public/components/cron_editor/index.d.ts | 26 +
.../public/components/cron_editor/index.js | 21 +
.../components/cron_editor}/services/cron.js | 19 +-
.../cron_editor/services/humanized_numbers.js | 91 ++++
.../components/cron_editor/services/index.js | 21 +
.../job_create_logistics.test.js | 70 +--
.../components/cron_editor/cron_hourly.js | 58 --
.../job_create/steps/components/index.js | 1 -
.../job_create/steps/step_logistics.js | 4 +-
.../sections/job_create/steps_config/index.js | 3 +-
.../crud_app/services/humanized_numbers.js | 78 ---
.../rollup/public/crud_app/services/index.js | 17 -
.../snapshot_restore/common/constants.ts | 1 +
.../snapshot_restore/common/lib/index.ts | 6 +
.../lib/policy_serialization.test.ts | 0
.../lib/policy_serialization.ts | 33 +-
.../lib/snapshot_serialization.test.ts | 0
.../lib/snapshot_serialization.ts | 26 +-
.../snapshot_restore/common/types/policy.ts | 25 +-
.../snapshot_restore/common/types/snapshot.ts | 4 +-
.../snapshot_restore/public/app/app.tsx | 14 +-
.../public/app/components/index.ts | 1 +
.../components/policy_execute_provider.tsx | 13 +-
.../components/policy_form/_policy_form.scss | 16 +
.../app/components/policy_form/index.ts | 6 +
.../app/components/policy_form/navigation.tsx | 55 ++
.../components/policy_form/policy_form.tsx | 217 ++++++++
.../app/components/policy_form/steps/index.ts | 22 +
.../policy_form/steps/step_logistics.tsx | 507 ++++++++++++++++++
.../policy_form/steps/step_review.tsx | 329 ++++++++++++
.../policy_form/steps/step_settings.tsx | 454 ++++++++++++++++
.../components/repository_delete_provider.tsx | 6 +-
.../steps/step_logistics.tsx | 8 +-
.../public/app/components/section_error.tsx | 13 +-
.../public/app/components/section_loading.tsx | 28 +-
.../public/app/constants/index.ts | 7 +
.../snapshot_restore/public/app/index.scss | 1 +
.../public/app/sections/home/_home.scss | 10 +
.../public/app/sections/home/home.tsx | 9 +-
.../policy_details/policy_details.tsx | 194 +++++--
.../policy_details/tabs/tab_history.tsx | 22 +-
.../policy_details/tabs/tab_summary.tsx | 124 +++--
.../sections/home/policy_list/policy_list.tsx | 112 +++-
.../policy_list/policy_table/policy_table.tsx | 198 ++++---
.../home/repository_list/repository_list.tsx | 9 +-
.../home/restore_list/restore_list.tsx | 3 +-
.../snapshot_details/tabs/tab_summary.tsx | 9 +-
.../home/snapshot_list/snapshot_list.tsx | 171 +++---
.../public/app/sections/index.ts | 2 +
.../public/app/sections/policy_add/index.ts} | 2 +-
.../app/sections/policy_add/policy_add.tsx | 132 +++++
.../public/app/sections/policy_edit/index.ts | 7 +
.../app/sections/policy_edit/policy_edit.tsx | 210 ++++++++
.../repository_add/repository_add.tsx | 14 +-
.../repository_edit/repository_edit.tsx | 5 +-
.../restore_snapshot/restore_snapshot.tsx | 5 +-
.../documentation/documentation_links.ts | 22 +-
.../app/services/http/policy_requests.ts | 43 +-
.../app/services/navigation/breadcrumb.ts | 144 +++--
.../app/services/navigation/doc_title.ts | 24 +
.../public/app/services/navigation/index.ts | 1 +
.../public/app/services/navigation/links.ts | 26 +-
.../public/app/services/text/text.ts | 18 +
.../public/app/services/validation/index.ts | 2 +
.../services/validation/validate_policy.ts | 96 ++++
.../plugins/snapshot_restore/public/plugin.ts | 11 +-
.../snapshot_restore/public/shared_imports.ts | 5 +
.../plugins/snapshot_restore/public/shim.ts | 9 +
.../server/client/elasticsearch_slm.ts | 14 +
.../snapshot_restore/server/lib/index.ts | 4 +-
.../snapshot_restore/server/routes/api/app.ts | 3 +-
.../server/routes/api/policy.test.ts | 116 +++-
.../server/routes/api/policy.ts | 69 ++-
.../server/routes/api/snapshots.test.ts | 13 +-
.../server/routes/api/snapshots.ts | 18 +-
.../translations/translations/ja-JP.json | 39 --
.../translations/translations/zh-CN.json | 39 --
84 files changed, 3696 insertions(+), 708 deletions(-)
rename {x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps => src/plugins/es_ui_shared/public}/components/cron_editor/cron_daily.js (60%)
rename {x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps => src/plugins/es_ui_shared/public}/components/cron_editor/cron_editor.js (88%)
create mode 100644 src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js
rename {x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps => src/plugins/es_ui_shared/public}/components/cron_editor/cron_monthly.js (63%)
rename {x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps => src/plugins/es_ui_shared/public}/components/cron_editor/cron_weekly.js (63%)
rename {x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps => src/plugins/es_ui_shared/public}/components/cron_editor/cron_yearly.js (65%)
create mode 100644 src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts
create mode 100644 src/plugins/es_ui_shared/public/components/cron_editor/index.js
rename {x-pack/legacy/plugins/rollup/public/crud_app => src/plugins/es_ui_shared/public/components/cron_editor}/services/cron.js (55%)
create mode 100644 src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js
create mode 100644 src/plugins/es_ui_shared/public/components/cron_editor/services/index.js
delete mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_hourly.js
delete mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/services/humanized_numbers.js
rename x-pack/legacy/plugins/snapshot_restore/{server => common}/lib/policy_serialization.test.ts (100%)
rename x-pack/legacy/plugins/snapshot_restore/{server => common}/lib/policy_serialization.ts (70%)
rename x-pack/legacy/plugins/snapshot_restore/{server => common}/lib/snapshot_serialization.test.ts (100%)
rename x-pack/legacy/plugins/snapshot_restore/{server => common}/lib/snapshot_serialization.ts (80%)
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/_policy_form.scss
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/index.ts
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
rename x-pack/legacy/plugins/{rollup/public/crud_app/sections/job_create/steps/components/cron_editor/index.js => snapshot_restore/public/app/sections/policy_add/index.ts} (84%)
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/index.ts
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/doc_title.ts
create mode 100644 x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
diff --git a/.i18nrc.json b/.i18nrc.json
index 29d4bb6f0fa29..81f043a42e259 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -27,7 +27,8 @@
"tsvb": "src/legacy/core_plugins/metrics",
"kbnESQuery": "packages/kbn-es-query",
"inspector": "src/plugins/inspector",
- "kibana-react": "src/plugins/kibana_react"
+ "kibana-react": "src/plugins/kibana_react",
+ "esUi": "src/plugins/es_ui_shared"
},
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
"translations": []
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_daily.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js
similarity index 60%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_daily.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js
index 8199ea8bf0b21..de14cd43165c2 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_daily.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { Fragment } from 'react';
@@ -27,12 +40,12 @@ export const CronDaily = ({
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
@@ -45,13 +58,13 @@ export const CronDaily = ({
)}
- data-test-subj="rollupJobCreateFrequencyDailyHourSelect"
+ data-test-subj="cronFrequencyDailyHourSelect"
/>
@@ -68,7 +81,7 @@ export const CronDaily = ({
)}
- data-test-subj="rollupJobCreateFrequencyDailyMinuteSelect"
+ data-test-subj="cronFrequencyDailyMinuteSelect"
/>
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_editor.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js
similarity index 88%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_editor.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js
index c0eb5bb624487..64d6405603dd7 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_editor.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { Component, Fragment } from 'react';
@@ -27,7 +40,7 @@ import {
WEEK,
MONTH,
YEAR,
-} from '../../../../../services';
+} from './services';
import { CronHourly } from './cron_hourly';
import { CronDaily } from './cron_daily';
@@ -331,7 +344,7 @@ export class CronEditor extends Component {
)}
@@ -346,13 +359,13 @@ export class CronEditor extends Component {
)}
- data-test-subj="rollupJobCreateFrequencySelect"
+ data-test-subj="cronFrequencySelect"
/>
diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js
new file mode 100644
index 0000000000000..a207998a7f73b
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js
@@ -0,0 +1,71 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import {
+ EuiFormRow,
+ EuiSelect,
+ EuiText,
+} from '@elastic/eui';
+
+export const CronHourly = ({
+ minute,
+ minuteOptions,
+ onChange,
+}) => (
+
+
+ )}
+ fullWidth
+ data-test-subj="cronFrequencyConfiguration"
+ >
+ onChange({ minute: e.target.value })}
+ fullWidth
+ prepend={(
+
+
+
+
+
+ )}
+ data-test-subj="cronFrequencyHourlyMinuteSelect"
+ />
+
+
+);
+
+CronHourly.propTypes = {
+ minute: PropTypes.string.isRequired,
+ minuteOptions: PropTypes.array.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_monthly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js
similarity index 63%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_monthly.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js
index 52a7701e4422e..e90a194d83d93 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_monthly.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { Fragment } from 'react';
@@ -29,12 +42,12 @@ export const CronMonthly = ({
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
)}
- data-test-subj="rollupJobCreateFrequencyMonthlyDateSelect"
+ data-test-subj="cronFrequencyMonthlyDateSelect"
/>
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
@@ -76,13 +89,13 @@ export const CronMonthly = ({
)}
- data-test-subj="rollupJobCreateFrequencyMonthlyHourSelect"
+ data-test-subj="cronFrequencyMonthlyHourSelect"
/>
@@ -99,7 +112,7 @@ export const CronMonthly = ({
)}
- data-test-subj="rollupJobCreateFrequencyMonthlyMinuteSelect"
+ data-test-subj="cronFrequencyMonthlyMinuteSelect"
/>
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_weekly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js
similarity index 63%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_weekly.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js
index 8c41f366bb2be..fbf9e37e46b48 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_weekly.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { Fragment } from 'react';
@@ -29,12 +42,12 @@ export const CronWeekly = ({
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
)}
- data-test-subj="rollupJobCreateFrequencyWeeklyDaySelect"
+ data-test-subj="cronFrequencyWeeklyDaySelect"
/>
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
@@ -76,13 +89,13 @@ export const CronWeekly = ({
)}
- data-test-subj="rollupJobCreateFrequencyWeeklyHourSelect"
+ data-test-subj="cronFrequencyWeeklyHourSelect"
/>
@@ -99,7 +112,7 @@ export const CronWeekly = ({
)}
- data-test-subj="rollupJobCreateFrequencyWeeklyMinuteSelect"
+ data-test-subj="cronFrequencyWeeklyMinuteSelect"
/>
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_yearly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js
similarity index 65%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_yearly.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js
index 900e77f63accb..5e19ec7b35b0c 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_yearly.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { Fragment } from 'react';
@@ -31,12 +44,12 @@ export const CronYearly = ({
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
)}
- data-test-subj="rollupJobCreateFrequencyYearlyMonthSelect"
+ data-test-subj="cronFrequencyYearlyMonthSelect"
/>
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
)}
- data-test-subj="rollupJobCreateFrequencyYearlyDateSelect"
+ data-test-subj="cronFrequencyYearlyDateSelect"
/>
)}
fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
+ data-test-subj="cronFrequencyConfiguration"
>
@@ -107,13 +120,13 @@ export const CronYearly = ({
)}
- data-test-subj="rollupJobCreateFrequencyYearlyHourSelect"
+ data-test-subj="cronFrequencyYearlyHourSelect"
/>
@@ -130,7 +143,7 @@ export const CronYearly = ({
)}
- data-test-subj="rollupJobCreateFrequencyYearlyMinuteSelect"
+ data-test-subj="cronFrequencyYearlyMinuteSelect"
/>
diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts b/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts
new file mode 100644
index 0000000000000..b318587057c76
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export declare const MINUTE: string;
+export declare const HOUR: string;
+export declare const DAY: string;
+export declare const WEEK: string;
+export declare const MONTH: string;
+export declare const YEAR: string;
+export declare const CronEditor: any;
diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.js b/src/plugins/es_ui_shared/public/components/cron_editor/index.js
new file mode 100644
index 0000000000000..6c4539a6c3f75
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { CronEditor } from './cron_editor';
+export { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './services';
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/cron.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js
similarity index 55%
rename from x-pack/legacy/plugins/rollup/public/crud_app/services/cron.js
rename to src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js
index 97e474f8df27c..71f6253375ef1 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/services/cron.js
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js
@@ -1,7 +1,20 @@
/*
- * 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.
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
export const MINUTE = 'MINUTE';
diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js
new file mode 100644
index 0000000000000..b3cb58bea24e5
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js
@@ -0,0 +1,91 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+// The international ISO standard dictates Monday as the first day of the week, but cron patterns
+// use Sunday as the first day, so we're going with the cron way.
+const dayOrdinalToDayNameMap = {
+ 0: i18n.translate('esUi.cronEditor.day.sunday', { defaultMessage: 'Sunday' }),
+ 1: i18n.translate('esUi.cronEditor.day.monday', { defaultMessage: 'Monday' }),
+ 2: i18n.translate('esUi.cronEditor.day.tuesday', { defaultMessage: 'Tuesday' }),
+ 3: i18n.translate('esUi.cronEditor.day.wednesday', { defaultMessage: 'Wednesday' }),
+ 4: i18n.translate('esUi.cronEditor.day.thursday', { defaultMessage: 'Thursday' }),
+ 5: i18n.translate('esUi.cronEditor.day.friday', { defaultMessage: 'Friday' }),
+ 6: i18n.translate('esUi.cronEditor.day.saturday', { defaultMessage: 'Saturday' }),
+};
+
+const monthOrdinalToMonthNameMap = {
+ 0: i18n.translate('esUi.cronEditor.month.january', { defaultMessage: 'January' }),
+ 1: i18n.translate('esUi.cronEditor.month.february', { defaultMessage: 'February' }),
+ 2: i18n.translate('esUi.cronEditor.month.march', { defaultMessage: 'March' }),
+ 3: i18n.translate('esUi.cronEditor.month.april', { defaultMessage: 'April' }),
+ 4: i18n.translate('esUi.cronEditor.month.may', { defaultMessage: 'May' }),
+ 5: i18n.translate('esUi.cronEditor.month.june', { defaultMessage: 'June' }),
+ 6: i18n.translate('esUi.cronEditor.month.july', { defaultMessage: 'July' }),
+ 7: i18n.translate('esUi.cronEditor.month.august', { defaultMessage: 'August' }),
+ 8: i18n.translate('esUi.cronEditor.month.september', { defaultMessage: 'September' }),
+ 9: i18n.translate('esUi.cronEditor.month.october', { defaultMessage: 'October' }),
+ 10: i18n.translate('esUi.cronEditor.month.november', { defaultMessage: 'November' }),
+ 11: i18n.translate('esUi.cronEditor.month.december', { defaultMessage: 'December' }),
+};
+
+export function getOrdinalValue(number) {
+ // TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale,
+ // which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings.
+ // return i18n.translate('esUi.cronEditor.number.ordinal', {
+ // defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
+ // values: { number },
+ // });
+ // TODO: https://github.com/elastic/kibana/issues/27136
+
+ // Protects against falsey (including 0) values
+ const num = number && number.toString();
+ let lastDigit = num && num.substr(-1);
+ let ordinal;
+
+ if(!lastDigit) {
+ return number;
+ }
+ lastDigit = parseFloat(lastDigit);
+
+ switch(lastDigit) {
+ case 1:
+ ordinal = 'st';
+ break;
+ case 2:
+ ordinal = 'nd';
+ break;
+ case 3:
+ ordinal = 'rd';
+ break;
+ default:
+ ordinal = 'th';
+ }
+
+ return `${num}${ordinal}`;
+}
+
+export function getDayName(dayOrdinal) {
+ return dayOrdinalToDayNameMap[dayOrdinal];
+}
+
+export function getMonthName(monthOrdinal) {
+ return monthOrdinalToMonthNameMap[monthOrdinal];
+}
diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js
new file mode 100644
index 0000000000000..cb4af15bf1945
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './cron';
+export * from './humanized_numbers';
diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js
index 82911650bf37a..f392f16abc31d 100644
--- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js
+++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from '../../public/crud_app/services';
+import { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from '../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../../src/legacy/ui/public/index_patterns';
import { setupEnvironment, pageHelpers } from './helpers';
@@ -162,7 +162,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
describe('rollup cron', () => {
const changeFrequency = (value) => {
- find('rollupJobCreateFrequencySelect').simulate('change', { target: { value } });
+ find('cronFrequencySelect').simulate('change', { target: { value } });
};
const generateStringSequenceOfNumbers = (total) => (
@@ -171,7 +171,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
describe('frequency', () => {
it('should allow "minute", "hour", "day", "week", "month", "year"', () => {
- const frequencySelect = find('rollupJobCreateFrequencySelect');
+ const frequencySelect = find('cronFrequencySelect');
const options = frequencySelect.find('option').map(option => option.text());
expect(options).toEqual(['minute', 'hour', 'day', 'week', 'month', 'year']);
});
@@ -179,7 +179,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
describe('every minute', () => {
it('should not have any additional configuration', () => {
changeFrequency(MINUTE);
- expect(find('rollupCronFrequencyConfiguration').length).toBe(0);
+ expect(find('cronFrequencyConfiguration').length).toBe(0);
});
});
@@ -189,12 +189,12 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should have 1 additional configuration', () => {
- expect(find('rollupCronFrequencyConfiguration').length).toBe(1);
- expect(exists('rollupJobCreateFrequencyHourlyMinuteSelect')).toBe(true);
+ expect(find('cronFrequencyConfiguration').length).toBe(1);
+ expect(exists('cronFrequencyHourlyMinuteSelect')).toBe(true);
});
it('should allow to select any minute from 00 -> 59', () => {
- const minutSelect = find('rollupJobCreateFrequencyHourlyMinuteSelect');
+ const minutSelect = find('cronFrequencyHourlyMinuteSelect');
const options = minutSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(60));
});
@@ -206,19 +206,19 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should have 1 additional configuration with hour and minute selects', () => {
- expect(find('rollupCronFrequencyConfiguration').length).toBe(1);
- expect(exists('rollupJobCreateFrequencyDailyHourSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyDailyMinuteSelect')).toBe(true);
+ expect(find('cronFrequencyConfiguration').length).toBe(1);
+ expect(exists('cronFrequencyDailyHourSelect')).toBe(true);
+ expect(exists('cronFrequencyDailyMinuteSelect')).toBe(true);
});
it('should allow to select any hour from 00 -> 23', () => {
- const hourSelect = find('rollupJobCreateFrequencyDailyHourSelect');
+ const hourSelect = find('cronFrequencyDailyHourSelect');
const options = hourSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(24));
});
it('should allow to select any miute from 00 -> 59', () => {
- const minutSelect = find('rollupJobCreateFrequencyDailyMinuteSelect');
+ const minutSelect = find('cronFrequencyDailyMinuteSelect');
const options = minutSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(60));
});
@@ -230,14 +230,14 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should have 2 additional configurations with day, hour and minute selects', () => {
- expect(find('rollupCronFrequencyConfiguration').length).toBe(2);
- expect(exists('rollupJobCreateFrequencyWeeklyDaySelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyWeeklyHourSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyWeeklyMinuteSelect')).toBe(true);
+ expect(find('cronFrequencyConfiguration').length).toBe(2);
+ expect(exists('cronFrequencyWeeklyDaySelect')).toBe(true);
+ expect(exists('cronFrequencyWeeklyHourSelect')).toBe(true);
+ expect(exists('cronFrequencyWeeklyMinuteSelect')).toBe(true);
});
it('should allow to select any day of the week', () => {
- const hourSelect = find('rollupJobCreateFrequencyWeeklyDaySelect');
+ const hourSelect = find('cronFrequencyWeeklyDaySelect');
const options = hourSelect.find('option').map(option => option.text());
expect(options).toEqual([
'Sunday',
@@ -251,13 +251,13 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should allow to select any hour from 00 -> 23', () => {
- const hourSelect = find('rollupJobCreateFrequencyWeeklyHourSelect');
+ const hourSelect = find('cronFrequencyWeeklyHourSelect');
const options = hourSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(24));
});
it('should allow to select any miute from 00 -> 59', () => {
- const minutSelect = find('rollupJobCreateFrequencyWeeklyMinuteSelect');
+ const minutSelect = find('cronFrequencyWeeklyMinuteSelect');
const options = minutSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(60));
});
@@ -269,26 +269,26 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should have 2 additional configurations with date, hour and minute selects', () => {
- expect(find('rollupCronFrequencyConfiguration').length).toBe(2);
- expect(exists('rollupJobCreateFrequencyMonthlyDateSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyMonthlyHourSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyMonthlyMinuteSelect')).toBe(true);
+ expect(find('cronFrequencyConfiguration').length).toBe(2);
+ expect(exists('cronFrequencyMonthlyDateSelect')).toBe(true);
+ expect(exists('cronFrequencyMonthlyHourSelect')).toBe(true);
+ expect(exists('cronFrequencyMonthlyMinuteSelect')).toBe(true);
});
it('should allow to select any date of the month from 1st to 31st', () => {
- const dateSelect = find('rollupJobCreateFrequencyMonthlyDateSelect');
+ const dateSelect = find('cronFrequencyMonthlyDateSelect');
const options = dateSelect.find('option').map(option => option.text());
expect(options.length).toEqual(31);
});
it('should allow to select any hour from 00 -> 23', () => {
- const hourSelect = find('rollupJobCreateFrequencyMonthlyHourSelect');
+ const hourSelect = find('cronFrequencyMonthlyHourSelect');
const options = hourSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(24));
});
it('should allow to select any miute from 00 -> 59', () => {
- const minutSelect = find('rollupJobCreateFrequencyMonthlyMinuteSelect');
+ const minutSelect = find('cronFrequencyMonthlyMinuteSelect');
const options = minutSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(60));
});
@@ -300,15 +300,15 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should have 3 additional configurations with month, date, hour and minute selects', () => {
- expect(find('rollupCronFrequencyConfiguration').length).toBe(3);
- expect(exists('rollupJobCreateFrequencyYearlyMonthSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyYearlyDateSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyYearlyHourSelect')).toBe(true);
- expect(exists('rollupJobCreateFrequencyYearlyMinuteSelect')).toBe(true);
+ expect(find('cronFrequencyConfiguration').length).toBe(3);
+ expect(exists('cronFrequencyYearlyMonthSelect')).toBe(true);
+ expect(exists('cronFrequencyYearlyDateSelect')).toBe(true);
+ expect(exists('cronFrequencyYearlyHourSelect')).toBe(true);
+ expect(exists('cronFrequencyYearlyMinuteSelect')).toBe(true);
});
it('should allow to select any month of the year', () => {
- const monthSelect = find('rollupJobCreateFrequencyYearlyMonthSelect');
+ const monthSelect = find('cronFrequencyYearlyMonthSelect');
const options = monthSelect.find('option').map(option => option.text());
expect(options).toEqual([
'January',
@@ -327,19 +327,19 @@ describe('Create Rollup Job, step 1: Logistics', () => {
});
it('should allow to select any date of the month from 1st to 31st', () => {
- const dateSelect = find('rollupJobCreateFrequencyYearlyDateSelect');
+ const dateSelect = find('cronFrequencyYearlyDateSelect');
const options = dateSelect.find('option').map(option => option.text());
expect(options.length).toEqual(31);
});
it('should allow to select any hour from 00 -> 23', () => {
- const hourSelect = find('rollupJobCreateFrequencyYearlyHourSelect');
+ const hourSelect = find('cronFrequencyYearlyHourSelect');
const options = hourSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(24));
});
it('should allow to select any miute from 00 -> 59', () => {
- const minutSelect = find('rollupJobCreateFrequencyYearlyMinuteSelect');
+ const minutSelect = find('cronFrequencyYearlyMinuteSelect');
const options = minutSelect.find('option').map(option => option.text());
expect(options).toEqual(generateStringSequenceOfNumbers(60));
});
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_hourly.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_hourly.js
deleted file mode 100644
index bab1704d4e721..0000000000000
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/cron_hourly.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 { FormattedMessage } from '@kbn/i18n/react';
-
-import {
- EuiFormRow,
- EuiSelect,
- EuiText,
-} from '@elastic/eui';
-
-export const CronHourly = ({
- minute,
- minuteOptions,
- onChange,
-}) => (
-
-
- )}
- fullWidth
- data-test-subj="rollupCronFrequencyConfiguration"
- >
- onChange({ minute: e.target.value })}
- fullWidth
- prepend={(
-
-
-
-
-
- )}
- data-test-subj="rollupJobCreateFrequencyHourlyMinuteSelect"
- />
-
-
-);
-
-CronHourly.propTypes = {
- minute: PropTypes.string.isRequired,
- minuteOptions: PropTypes.array.isRequired,
- onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js
index e5c8eb2e2e17f..1efdcb7caec92 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js
+++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js
@@ -5,5 +5,4 @@
*/
export { FieldChooser } from './field_chooser';
-export { CronEditor } from './cron_editor';
export { StepError } from './step_error';
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js
index 382d1b7ccca47..62b0045395099 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js
+++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js
@@ -24,10 +24,12 @@ import {
EuiTitle,
} from '@elastic/eui';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { CronEditor } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns';
import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices';
import { logisticalDetailsUrl, cronUrl } from '../../../services';
-import { CronEditor, StepError } from './components';
+import { StepError } from './components';
const indexPatternIllegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
const indexIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js
index 09c427a49f028..db77844dcfe35 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js
+++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js
@@ -8,7 +8,8 @@ import cloneDeep from 'lodash/lang/cloneDeep';
import get from 'lodash/object/get';
import pick from 'lodash/object/pick';
-import { WEEK } from '../../../services';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { WEEK } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
import { validateId } from './validate_id';
import { validateIndexPattern } from './validate_index_pattern';
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/humanized_numbers.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/humanized_numbers.js
deleted file mode 100644
index ce779e62df926..0000000000000
--- a/x-pack/legacy/plugins/rollup/public/crud_app/services/humanized_numbers.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 { i18n } from '@kbn/i18n';
-
-// The international ISO standard dictates Monday as the first day of the week, but cron patterns
-// use Sunday as the first day, so we're going with the cron way.
-const dayOrdinalToDayNameMap = {
- 0: i18n.translate('xpack.rollupJobs.util.day.sunday', { defaultMessage: 'Sunday' }),
- 1: i18n.translate('xpack.rollupJobs.util.day.monday', { defaultMessage: 'Monday' }),
- 2: i18n.translate('xpack.rollupJobs.util.day.tuesday', { defaultMessage: 'Tuesday' }),
- 3: i18n.translate('xpack.rollupJobs.util.day.wednesday', { defaultMessage: 'Wednesday' }),
- 4: i18n.translate('xpack.rollupJobs.util.day.thursday', { defaultMessage: 'Thursday' }),
- 5: i18n.translate('xpack.rollupJobs.util.day.friday', { defaultMessage: 'Friday' }),
- 6: i18n.translate('xpack.rollupJobs.util.day.saturday', { defaultMessage: 'Saturday' }),
-};
-
-const monthOrdinalToMonthNameMap = {
- 0: i18n.translate('xpack.rollupJobs.util.month.january', { defaultMessage: 'January' }),
- 1: i18n.translate('xpack.rollupJobs.util.month.february', { defaultMessage: 'February' }),
- 2: i18n.translate('xpack.rollupJobs.util.month.march', { defaultMessage: 'March' }),
- 3: i18n.translate('xpack.rollupJobs.util.month.april', { defaultMessage: 'April' }),
- 4: i18n.translate('xpack.rollupJobs.util.month.may', { defaultMessage: 'May' }),
- 5: i18n.translate('xpack.rollupJobs.util.month.june', { defaultMessage: 'June' }),
- 6: i18n.translate('xpack.rollupJobs.util.month.july', { defaultMessage: 'July' }),
- 7: i18n.translate('xpack.rollupJobs.util.month.august', { defaultMessage: 'August' }),
- 8: i18n.translate('xpack.rollupJobs.util.month.september', { defaultMessage: 'September' }),
- 9: i18n.translate('xpack.rollupJobs.util.month.october', { defaultMessage: 'October' }),
- 10: i18n.translate('xpack.rollupJobs.util.month.november', { defaultMessage: 'November' }),
- 11: i18n.translate('xpack.rollupJobs.util.month.december', { defaultMessage: 'December' }),
-};
-
-export function getOrdinalValue(number) {
- // TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale,
- // which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings.
- // return i18n.translate('xpack.rollupJobs.util.number.ordinal', {
- // defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
- // values: { number },
- // });
- // TODO: https://github.com/elastic/kibana/issues/27136
-
- // Protects against falsey (including 0) values
- const num = number && number.toString();
- let lastDigit = num && num.substr(-1);
- let ordinal;
-
- if(!lastDigit) {
- return number;
- }
- lastDigit = parseFloat(lastDigit);
-
- switch(lastDigit) {
- case 1:
- ordinal = 'st';
- break;
- case 2:
- ordinal = 'nd';
- break;
- case 3:
- ordinal = 'rd';
- break;
- default:
- ordinal = 'th';
- }
-
- return `${num}${ordinal}`;
-}
-
-export function getDayName(dayOrdinal) {
- return dayOrdinalToDayNameMap[dayOrdinal];
-}
-
-export function getMonthName(monthOrdinal) {
- return monthOrdinalToMonthNameMap[monthOrdinal];
-}
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js
index b3a7cdb9a286d..c52c9064b8d76 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js
+++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js
@@ -23,17 +23,6 @@ export {
createBreadcrumb,
} from './breadcrumbs';
-export {
- cronExpressionToParts,
- cronPartsToExpression,
- MINUTE,
- HOUR,
- DAY,
- WEEK,
- MONTH,
- YEAR,
-} from './cron';
-
export {
logisticalDetailsUrl,
dateHistogramDetailsUrl,
@@ -61,12 +50,6 @@ export {
getHttp,
} from './http_provider';
-export {
- getOrdinalValue,
- getDayName,
- getMonthName,
-} from './humanized_numbers';
-
export {
serializeJob,
deserializeJob,
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/constants.ts b/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
index d876c6ffd581d..a881bf3081c5e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
@@ -53,3 +53,4 @@ export const APP_REQUIRED_CLUSTER_PRIVILEGES = [
'cluster:admin/repository',
];
export const APP_RESTORE_INDEX_PRIVILEGES = ['monitor'];
+export const APP_SLM_CLUSTER_PRIVILEGES = ['manage_slm'];
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
index 0092d37b74a20..bede2689bb855 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
@@ -8,3 +8,9 @@ export {
deserializeRestoreSettings,
serializeRestoreSettings,
} from './restore_settings_serialization';
+export {
+ deserializeSnapshotDetails,
+ deserializeSnapshotConfig,
+ serializeSnapshotConfig,
+} from './snapshot_serialization';
+export { deserializePolicy, serializePolicy } from './policy_serialization';
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/policy_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/snapshot_restore/server/lib/policy_serialization.test.ts
rename to x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/policy_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
similarity index 70%
rename from x-pack/legacy/plugins/snapshot_restore/server/lib/policy_serialization.ts
rename to x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
index 5abbc4270ec2f..dc52765670540 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/lib/policy_serialization.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
@@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { SlmPolicy, SlmPolicyEs } from '../../common/types';
-import { deserializeSnapshotConfig } from './';
+import { SlmPolicy, SlmPolicyEs, SlmPolicyPayload } from '../types';
+import { deserializeSnapshotConfig, serializeSnapshotConfig } from './';
export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolicy => {
const {
@@ -16,6 +16,7 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
next_execution_millis: nextExecutionMillis,
last_failure: lastFailure,
last_success: lastSuccess,
+ in_progress: inProgress,
} = esPolicy;
const policy: SlmPolicy = {
@@ -26,11 +27,14 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
snapshotName,
schedule,
repository,
- config: deserializeSnapshotConfig(config),
nextExecution,
nextExecutionMillis,
};
+ if (config) {
+ policy.config = deserializeSnapshotConfig(config);
+ }
+
if (lastFailure) {
const {
snapshot_name: failureSnapshotName,
@@ -70,5 +74,28 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
};
}
+ if (inProgress) {
+ const { name: inProgressSnapshotName } = inProgress;
+
+ policy.inProgress = {
+ snapshotName: inProgressSnapshotName,
+ };
+ }
+
return policy;
};
+
+export const serializePolicy = (policy: SlmPolicyPayload): SlmPolicyEs['policy'] => {
+ const { snapshotName: name, schedule, repository, config } = policy;
+ const policyEs: SlmPolicyEs['policy'] = {
+ name,
+ schedule,
+ repository,
+ };
+
+ if (config) {
+ policyEs.config = serializeSnapshotConfig(config);
+ }
+
+ return policyEs;
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/snapshot_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/snapshot_restore/server/lib/snapshot_serialization.test.ts
rename to x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/snapshot_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
similarity index 80%
rename from x-pack/legacy/plugins/snapshot_restore/server/lib/snapshot_serialization.ts
rename to x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
index 608d85cf8840b..b1f6d2005a2e3 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/lib/snapshot_serialization.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
@@ -6,12 +6,7 @@
import { sortBy } from 'lodash';
-import {
- SnapshotDetails,
- SnapshotDetailsEs,
- SnapshotConfig,
- SnapshotConfigEs,
-} from '../../common/types';
+import { SnapshotDetails, SnapshotDetailsEs, SnapshotConfig, SnapshotConfigEs } from '../types';
export function deserializeSnapshotDetails(
repository: string,
@@ -114,3 +109,22 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S
return config;
}, {});
}
+
+export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs {
+ const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig;
+
+ const snapshotConfigEs: SnapshotConfigEs = {
+ indices,
+ ignore_unavailable: ignoreUnavailable,
+ include_global_state: includeGlobalState,
+ partial,
+ metadata,
+ };
+
+ return Object.entries(snapshotConfigEs).reduce((config: any, [key, value]) => {
+ if (value !== undefined) {
+ config[key] = value;
+ }
+ return config;
+ }, {});
+}
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
index 54d17e853cc87..888cad13d213b 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
@@ -6,15 +6,18 @@
import { SnapshotConfig, SnapshotConfigEs } from './snapshot';
-export interface SlmPolicy {
+export interface SlmPolicyPayload {
name: string;
- version: number;
- modifiedDate: string;
- modifiedDateMillis: number;
snapshotName: string;
schedule: string;
repository: string;
- config: SnapshotConfig;
+ config?: SnapshotConfig;
+}
+
+export interface SlmPolicy extends SlmPolicyPayload {
+ version: number;
+ modifiedDate: string;
+ modifiedDateMillis: number;
nextExecution: string;
nextExecutionMillis: number;
lastSuccess?: {
@@ -28,6 +31,9 @@ export interface SlmPolicy {
time: number;
details: object | string;
};
+ inProgress?: {
+ snapshotName: string;
+ };
}
export interface SlmPolicyEs {
@@ -38,7 +44,7 @@ export interface SlmPolicyEs {
name: string;
schedule: string;
repository: string;
- config: SnapshotConfigEs;
+ config?: SnapshotConfigEs;
};
next_execution: string;
next_execution_millis: number;
@@ -53,4 +59,11 @@ export interface SlmPolicyEs {
time: number;
details: string;
};
+ in_progress?: {
+ name: string;
+ uuid: string;
+ state: string;
+ start_time: string;
+ start_time_millis: number;
+ };
}
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
index c896336cc943b..dd561bd50d352 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export interface SnapshotConfig {
- indices?: string[];
+ indices?: string | string[];
ignoreUnavailable?: boolean;
includeGlobalState?: boolean;
partial?: boolean;
@@ -14,7 +14,7 @@ export interface SnapshotConfig {
}
export interface SnapshotConfigEs {
- indices?: string[];
+ indices?: string | string[];
ignore_unavailable?: boolean;
include_global_state?: boolean;
partial?: boolean;
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx
index 207044c4692fd..764c50bc47721 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx
@@ -8,9 +8,17 @@ import React, { useContext } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { EuiPageContent } from '@elastic/eui';
+import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common/constants';
import { SectionLoading, SectionError } from './components';
import { BASE_PATH, DEFAULT_SECTION, Section } from './constants';
-import { RepositoryAdd, RepositoryEdit, RestoreSnapshot, SnapshotRestoreHome } from './sections';
+import {
+ RepositoryAdd,
+ RepositoryEdit,
+ RestoreSnapshot,
+ SnapshotRestoreHome,
+ PolicyAdd,
+ PolicyEdit,
+} from './sections';
import { useAppDependencies } from './index';
import { AuthorizationContext, WithPrivileges, NotAuthorizedSection } from './lib/authorization';
@@ -36,7 +44,7 @@ export const App: React.FunctionComponent = () => {
error={apiError}
/>
) : (
-
+ `cluster.${name}`)}>
{({ isLoading, hasPrivileges, privilegesMissing }) =>
isLoading ? (
@@ -69,6 +77,8 @@ export const App: React.FunctionComponent = () => {
path={`${BASE_PATH}/restore/:repositoryName/:snapshotId*`}
component={RestoreSnapshot}
/>
+
+
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts
index a017299a78914..a367e529cf63b 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts
@@ -16,3 +16,4 @@ export { SnapshotDeleteProvider } from './snapshot_delete_provider';
export { RestoreSnapshotForm } from './restore_snapshot_form';
export { PolicyExecuteProvider } from './policy_execute_provider';
export { PolicyDeleteProvider } from './policy_delete_provider';
+export { PolicyForm } from './policy_form';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx
index 3df081e9c9dba..c43ab02801e4e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx
@@ -87,7 +87,7 @@ export const PolicyExecuteProvider: React.FunctionComponent = ({ children
title={
}
@@ -102,18 +102,11 @@ export const PolicyExecuteProvider: React.FunctionComponent = ({ children
confirmButtonText={
}
data-test-subj="srExecutePolicyConfirmationModal"
- >
-
-
-
-
+ />
);
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/_policy_form.scss b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/_policy_form.scss
new file mode 100644
index 0000000000000..0a5187908f854
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/_policy_form.scss
@@ -0,0 +1,16 @@
+/*
+ * Prevent switch controls from moving around when toggling content
+ */
+.snapshotRestore__policyForm__stepSettings {
+ .euiFormRow--hasEmptyLabelSpace {
+ min-height: auto;
+ margin-top: $euiFontSizeXS + $euiSizeS + ($euiSizeXXL / 4);
+ }
+}
+
+/*
+ * Allow toggle mode link in indices field label to be flushed right
+ */
+.snapshotRestore__policyForm__stepSettings__indicesFieldWrapper .euiFormLabel {
+ width: 100%;
+}
\ No newline at end of file
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/index.ts
new file mode 100644
index 0000000000000..0da06da9e1f8e
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/index.ts
@@ -0,0 +1,6 @@
+/*
+ * 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 { PolicyForm } from './policy_form';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
new file mode 100644
index 0000000000000..ba9877a9e9f41
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
@@ -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 React from 'react';
+import { EuiStepsHorizontal } from '@elastic/eui';
+import { useAppDependencies } from '../../index';
+
+interface Props {
+ currentStep: number;
+ maxCompletedStep: number;
+ updateCurrentStep: (step: number) => void;
+}
+
+export const PolicyNavigation: React.FunctionComponent = ({
+ currentStep,
+ maxCompletedStep,
+ updateCurrentStep,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+
+ const steps = [
+ {
+ title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepLogisticsName', {
+ defaultMessage: 'Logistics',
+ }),
+ isComplete: maxCompletedStep >= 1,
+ isSelected: currentStep === 1,
+ onClick: () => updateCurrentStep(1),
+ },
+ {
+ title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepSettingsName', {
+ defaultMessage: 'Snapshot settings',
+ }),
+ isComplete: maxCompletedStep >= 2,
+ isSelected: currentStep === 2,
+ disabled: maxCompletedStep < 1,
+ onClick: () => updateCurrentStep(2),
+ },
+ {
+ title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepReviewName', {
+ defaultMessage: 'Review',
+ }),
+ isComplete: maxCompletedStep >= 2,
+ isSelected: currentStep === 3,
+ disabled: maxCompletedStep < 2,
+ onClick: () => updateCurrentStep(3),
+ },
+ ];
+
+ return ;
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
new file mode 100644
index 0000000000000..6c631ab8e6c69
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
@@ -0,0 +1,217 @@
+/*
+ * 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, useState } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiSpacer,
+} from '@elastic/eui';
+import { SlmPolicyPayload } from '../../../../common/types';
+import { PolicyValidation, validatePolicy } from '../../services/validation';
+import { useAppDependencies } from '../../index';
+import { PolicyStepLogistics, PolicyStepSettings, PolicyStepReview } from './steps';
+import { PolicyNavigation } from './navigation';
+
+interface Props {
+ policy: SlmPolicyPayload;
+ indices: string[];
+ currentUrl: string;
+ isEditing?: boolean;
+ isSaving: boolean;
+ saveError?: React.ReactNode;
+ clearSaveError: () => void;
+ onCancel: () => void;
+ onSave: (policy: SlmPolicyPayload) => void;
+}
+
+export const PolicyForm: React.FunctionComponent = ({
+ policy: originalPolicy,
+ indices,
+ currentUrl,
+ isEditing,
+ isSaving,
+ saveError,
+ clearSaveError,
+ onCancel,
+ onSave,
+}) => {
+ const {
+ core: {
+ i18n: { FormattedMessage },
+ },
+ } = useAppDependencies();
+
+ // Step state
+ const [currentStep, setCurrentStep] = useState(1);
+ const [maxCompletedStep, setMaxCompletedStep] = useState(0);
+ const stepMap: { [key: number]: any } = {
+ 1: PolicyStepLogistics,
+ 2: PolicyStepSettings,
+ 3: PolicyStepReview,
+ };
+ const CurrentStepForm = stepMap[currentStep];
+
+ // Policy state
+ const [policy, setPolicy] = useState({
+ ...originalPolicy,
+ config: {
+ ...(originalPolicy.config || {}),
+ },
+ });
+
+ // Policy validation state
+ const [validation, setValidation] = useState({
+ isValid: true,
+ errors: {},
+ });
+
+ const updatePolicy = (updatedFields: any): void => {
+ const newPolicy = { ...policy, ...updatedFields };
+ const newValidation = validatePolicy(newPolicy);
+ setPolicy(newPolicy);
+ setValidation(newValidation);
+ };
+
+ const updateCurrentStep = (step: number) => {
+ if (maxCompletedStep < step - 1) {
+ return;
+ }
+ setCurrentStep(step);
+ setMaxCompletedStep(step - 1);
+ clearSaveError();
+ };
+
+ const onBack = () => {
+ const previousStep = currentStep - 1;
+ setCurrentStep(previousStep);
+ setMaxCompletedStep(previousStep - 1);
+ clearSaveError();
+ };
+
+ const onNext = () => {
+ if (!validation.isValid) {
+ return;
+ }
+ const nextStep = currentStep + 1;
+ setMaxCompletedStep(Math.max(currentStep, maxCompletedStep));
+ setCurrentStep(nextStep);
+ };
+
+ const savePolicy = () => {
+ if (validation.isValid) {
+ onSave(policy);
+ }
+ };
+
+ const lastStep = Object.keys(stepMap).length;
+
+ return (
+
+
+
+
+
+
+
+ {saveError ? (
+
+ {saveError}
+
+
+ ) : null}
+
+
+
+
+ {currentStep > 1 ? (
+
+ onBack()}
+ disabled={!validation.isValid}
+ >
+
+
+
+ ) : null}
+ {currentStep < lastStep ? (
+
+ onNext()}
+ disabled={!validation.isValid}
+ >
+
+
+
+ ) : null}
+ {currentStep === lastStep ? (
+
+ savePolicy()}
+ isLoading={isSaving}
+ >
+ {isSaving ? (
+
+ ) : isEditing ? (
+
+ ) : (
+
+ )}
+
+
+ ) : null}
+
+
+
+
+ onCancel()}>
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts
new file mode 100644
index 0000000000000..10dd696e3424f
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { SlmPolicyPayload } from '../../../../../common/types';
+import { PolicyValidation } from '../../../services/validation';
+
+export interface StepProps {
+ policy: SlmPolicyPayload;
+ indices: string[];
+ updatePolicy: (updatedSettings: Partial) => void;
+ isEditing: boolean;
+ currentUrl: string;
+ errors: PolicyValidation['errors'];
+ updateCurrentStep: (step: number) => void;
+}
+
+export { PolicyStepLogistics } from './step_logistics';
+export { PolicyStepSettings } from './step_settings';
+export { PolicyStepReview } from './step_review';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx
new file mode 100644
index 0000000000000..f96eb5347bc18
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx
@@ -0,0 +1,507 @@
+/*
+ * 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, useState } from 'react';
+
+import {
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiFormRow,
+ EuiFieldText,
+ EuiSelect,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiLink,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+
+import { Repository } from '../../../../../common/types';
+import { CronEditor } from '../../../../shared_imports';
+import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants';
+import { useLoadRepositories } from '../../../services/http';
+import { linkToAddRepository } from '../../../services/navigation';
+import { documentationLinksService } from '../../../services/documentation';
+import { useAppDependencies } from '../../../index';
+import { SectionLoading, SectionError } from '../../';
+import { StepProps } from './';
+
+export const PolicyStepLogistics: React.FunctionComponent = ({
+ policy,
+ updatePolicy,
+ isEditing,
+ currentUrl,
+ errors,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+
+ // Load repositories for repository dropdown field
+ const {
+ error: errorLoadingRepositories,
+ isLoading: isLoadingRepositories,
+ data: { repositories } = {
+ repositories: [],
+ },
+ sendRequest: reloadRepositories,
+ } = useLoadRepositories();
+
+ // State for touched inputs
+ const [touched, setTouched] = useState({
+ name: false,
+ snapshotName: false,
+ repository: false,
+ schedule: false,
+ });
+
+ // State for cron editor
+ const [simpleCron, setSimpleCron] = useState<{
+ expression: string;
+ frequency: string;
+ }>({
+ expression: DEFAULT_POLICY_SCHEDULE,
+ frequency: DEFAULT_POLICY_FREQUENCY,
+ });
+ const [isAdvancedCronVisible, setIsAdvancedCronVisible] = useState(
+ Boolean(policy.schedule && policy.schedule !== DEFAULT_POLICY_SCHEDULE)
+ );
+ const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState({});
+
+ const renderNameField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="nameDescription"
+ fullWidth
+ >
+
+ }
+ describedByIds={['nameDescription']}
+ isInvalid={touched.name && Boolean(errors.name)}
+ error={errors.name}
+ fullWidth
+ >
+ setTouched({ ...touched, name: true })}
+ onChange={e => {
+ updatePolicy({
+ name: e.target.value,
+ });
+ }}
+ placeholder={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepLogistics.namePlaceholder',
+ {
+ defaultMessage: 'daily-snapshots',
+ description:
+ 'Example SLM policy name. Similar to index names, do not use spaces in translation.',
+ }
+ )}
+ data-test-subj="nameInput"
+ disabled={isEditing}
+ />
+
+
+ );
+
+ const renderRepositoryField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="policyRepositoryDescription"
+ fullWidth
+ >
+
+ }
+ describedByIds={['policyRepositoryDescription']}
+ isInvalid={touched.repository && Boolean(errors.repository)}
+ error={errors.repository}
+ fullWidth
+ >
+ {renderRepositorySelect()}
+
+
+ );
+
+ const renderRepositorySelect = () => {
+ if (isLoadingRepositories) {
+ return (
+
+
+
+ );
+ }
+
+ if (errorLoadingRepositories) {
+ return (
+
+ }
+ error={{ data: { error: 'test' } } || errorLoadingRepositories}
+ actions={
+ reloadRepositories()}
+ color="danger"
+ iconType="refresh"
+ data-test-subj="reloadRepositoriesButton"
+ >
+
+
+ }
+ />
+ );
+ }
+
+ if (repositories.length === 0) {
+ return (
+
+ }
+ error={{
+ data: {
+ error: i18n.translate('xpack.snapshotRestore.policyForm.noRepositoriesErrorMessage', {
+ defaultMessage: 'You must register a repository to store your snapshots.',
+ }),
+ },
+ }}
+ actions={
+
+
+
+ }
+ />
+ );
+ } else {
+ if (!policy.repository) {
+ updatePolicy({
+ repository: repositories[0].name,
+ });
+ }
+ }
+
+ return (
+ ({
+ value: name,
+ text: name,
+ }))}
+ value={policy.repository || repositories[0].name}
+ onBlur={() => setTouched({ ...touched, repository: true })}
+ onChange={e => {
+ updatePolicy({
+ repository: e.target.value,
+ });
+ }}
+ fullWidth
+ data-test-subj="repositorySelect"
+ />
+ );
+ };
+
+ const renderSnapshotNameField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="policySnapshotNameDescription"
+ fullWidth
+ >
+
+ }
+ describedByIds={['policySnapshotNameDescription']}
+ isInvalid={touched.snapshotName && Boolean(errors.snapshotName)}
+ error={errors.snapshotName}
+ helpText={
+
+
+
+ ),
+ }}
+ />
+ }
+ fullWidth
+ >
+ {
+ updatePolicy({
+ snapshotName: e.target.value.toLowerCase(),
+ });
+ }}
+ onBlur={() => setTouched({ ...touched, snapshotName: true })}
+ placeholder={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepLogistics.policySnapshotNamePlaceholder',
+ {
+ defaultMessage: '',
+ description:
+ 'Example date math snapshot name. Keeping the same syntax is important: ',
+ }
+ )}
+ data-test-subj="snapshotNameInput"
+ />
+
+
+ );
+
+ const renderScheduleField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="policyScheduleDescription"
+ fullWidth
+ >
+ {isAdvancedCronVisible ? (
+
+
+ }
+ describedByIds={['policyScheduleDescription']}
+ isInvalid={touched.schedule && Boolean(errors.schedule)}
+ error={errors.schedule}
+ helpText={
+
+
+
+ ),
+ }}
+ />
+ }
+ fullWidth
+ >
+ {
+ updatePolicy({
+ schedule: e.target.value,
+ });
+ }}
+ onBlur={() => setTouched({ ...touched, schedule: true })}
+ placeholder={DEFAULT_POLICY_SCHEDULE}
+ data-test-subj="snapshotNameInput"
+ />
+
+
+
+ {
+ setIsAdvancedCronVisible(false);
+ updatePolicy({
+ schedule: simpleCron.expression,
+ });
+ }}
+ data-test-subj="showBasicCronLink"
+ >
+
+
+
+
+ ) : (
+
+ {
+ setSimpleCron({
+ expression,
+ frequency,
+ });
+ setFieldToPreferredValueMap(newFieldToPreferredValueMap);
+ updatePolicy({
+ schedule: expression,
+ });
+ }}
+ />
+
+
+ {
+ setIsAdvancedCronVisible(true);
+ }}
+ data-test-subj="showAdvancedCronLink"
+ >
+
+
+
+
+ )}
+
+ );
+
+ return (
+
+ {/* Step title and doc link */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderNameField()}
+ {renderSnapshotNameField()}
+ {renderRepositoryField()}
+ {renderScheduleField()}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
new file mode 100644
index 0000000000000..2599aa4b19bb1
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
@@ -0,0 +1,329 @@
+/*
+ * 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, useState } from 'react';
+import {
+ EuiCodeBlock,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiSpacer,
+ EuiTabbedContent,
+ EuiTitle,
+ EuiLink,
+ EuiIcon,
+ EuiToolTip,
+ EuiText,
+} from '@elastic/eui';
+import { serializePolicy } from '../../../../../common/lib';
+import { useAppDependencies } from '../../../index';
+import { StepProps } from './';
+
+export const PolicyStepReview: React.FunctionComponent = ({
+ policy,
+ updateCurrentStep,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+ const { name, snapshotName, schedule, repository, config } = policy;
+ const { indices, includeGlobalState, ignoreUnavailable, partial } = config || {
+ indices: undefined,
+ includeGlobalState: undefined,
+ ignoreUnavailable: undefined,
+ partial: undefined,
+ };
+
+ const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState(false);
+ const displayIndices = indices
+ ? typeof indices === 'string'
+ ? indices.split(',')
+ : indices
+ : undefined;
+ const hiddenIndicesCount =
+ displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0;
+
+ const renderSummaryTab = () => (
+
+
+
+
+ {' '}
+
+ }
+ >
+ updateCurrentStep(1)}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+ {snapshotName}
+
+
+
+
+
+
+
+
+
+
+
+ {repository}
+
+
+
+
+
+
+
+ {schedule}
+
+
+
+
+
+
+
+ {' '}
+
+ }
+ >
+ updateCurrentStep(2)}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {displayIndices ? (
+
+
+ {(isShowingFullIndicesList
+ ? displayIndices
+ : [...displayIndices].splice(0, 10)
+ ).map(index => (
+ -
+
+ {index}
+
+
+ ))}
+ {hiddenIndicesCount ? (
+ -
+
+ {isShowingFullIndicesList ? (
+ setIsShowingFullIndicesList(false)}>
+ {' '}
+
+
+ ) : (
+ setIsShowingFullIndicesList(true)}>
+ {' '}
+
+
+ )}
+
+
+ ) : null}
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {ignoreUnavailable ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {partial ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {includeGlobalState === false ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+
+ const renderRequestTab = () => {
+ const endpoint = `PUT _slm/policy/${name}`;
+ const json = JSON.stringify(serializePolicy(policy), null, 2);
+ return (
+
+
+
+ {`${endpoint}\n${json}`}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
new file mode 100644
index 0000000000000..642440a8c5e91
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
@@ -0,0 +1,454 @@
+/*
+ * 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, useState } from 'react';
+
+import {
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiFormRow,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiSwitch,
+ EuiLink,
+ EuiSelectable,
+ EuiPanel,
+ EuiComboBox,
+} from '@elastic/eui';
+import { Option } from '@elastic/eui/src/components/selectable/types';
+import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types';
+import { documentationLinksService } from '../../../services/documentation';
+import { useAppDependencies } from '../../../index';
+import { StepProps } from './';
+
+export const PolicyStepSettings: React.FunctionComponent = ({
+ policy,
+ indices,
+ updatePolicy,
+ errors,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+ const { config = {} } = policy;
+
+ const updatePolicyConfig = (updatedFields: Partial): void => {
+ const newConfig = { ...config, ...updatedFields };
+ updatePolicy({
+ config: newConfig,
+ });
+ };
+
+ // States for choosing all indices, or a subset, including caching previously chosen subset list
+ const [isAllIndices, setIsAllIndices] = useState(!Boolean(config.indices));
+ const [indicesSelection, setIndicesSelection] = useState([...indices]);
+ const [indicesOptions, setIndicesOptions] = useState
}
+ actions={
+
+
+
+ }
data-test-subj="emptyPrompt"
/>
);
} else {
+ const policySchedules = policies.map((policy: SlmPolicy) => policy.schedule);
+ const hasDuplicateSchedules = policySchedules.length > new Set(policySchedules).size;
content = (
-
+
+ {hasDuplicateSchedules ? (
+
+
+ }
+ color="warning"
+ iconType="alert"
+ >
+
+
+
+
+ ) : null}
+
+
);
}
return (
-
- {policyName ? (
-
- ) : null}
- {content}
-
+ `cluster.${name}`)}>
+ {({ hasPrivileges, privilegesMissing }) =>
+ hasPrivileges ? (
+
+ {policyName ? (
+
+ ) : null}
+ {content}
+
+ ) : (
+
+ }
+ message={
+
+ }
+ />
+ )
+ }
+
);
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx
index 7db94b47c3ab6..2382f16e1f894 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx
@@ -13,6 +13,7 @@ import {
EuiLink,
EuiToolTip,
EuiButtonIcon,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { SlmPolicy } from '../../../../../../common/types';
@@ -24,6 +25,7 @@ import {
PolicyDeleteProvider,
} from '../../../../components';
import { uiMetricService } from '../../../../services/ui_metric';
+import { linkToAddPolicy, linkToEditPolicy } from '../../../../services/navigation';
interface Props {
policies: SlmPolicy[];
@@ -55,16 +57,34 @@ export const PolicyTable: React.FunctionComponent = ({
}),
truncateText: true,
sortable: true,
- render: (name: SlmPolicy['name']) => {
+ render: (name: SlmPolicy['name'], { inProgress }: SlmPolicy) => {
return (
- /* eslint-disable-next-line @elastic/eui/href-or-on-click */
- trackUiMetric(UIM_POLICY_SHOW_DETAILS_CLICK)}
- href={openPolicyDetailsUrl(name)}
- data-test-subj="policyLink"
- >
- {name}
-
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+ trackUiMetric(UIM_POLICY_SHOW_DETAILS_CLICK)}
+ href={openPolicyDetailsUrl(name)}
+ data-test-subj="policyLink"
+ >
+ {name}
+
+
+ {inProgress ? (
+
+
+
+
+
+ ) : null}
+
);
},
},
@@ -95,7 +115,7 @@ export const PolicyTable: React.FunctionComponent = ({
{
field: 'nextExecutionMillis',
name: i18n.translate('xpack.snapshotRestore.policyList.table.nextExecutionColumnTitle', {
- defaultMessage: 'Next execution',
+ defaultMessage: 'Next snapshot',
}),
truncateText: true,
sortable: true,
@@ -109,64 +129,96 @@ export const PolicyTable: React.FunctionComponent = ({
}),
actions: [
{
- render: ({ name }: SlmPolicy) => {
- return (
-
- {executePolicyPrompt => {
- const label = i18n.translate(
- 'xpack.snapshotRestore.policyList.table.actionExecuteTooltip',
- { defaultMessage: 'Run policy' }
- );
- return (
-
- executePolicyPrompt(name, onPolicyExecuted)}
- />
-
- );
- }}
-
- );
- },
- },
- {
- render: ({ name }: SlmPolicy) => {
+ render: ({ name, inProgress }: SlmPolicy) => {
return (
-
- {deletePolicyPrompt => {
- const label = i18n.translate(
- 'xpack.snapshotRestore.policyList.table.actionDeleteTooltip',
- { defaultMessage: 'Delete' }
- );
- return (
-
-
+
+
+ {executePolicyPrompt => {
+ return (
+ deletePolicyPrompt([name], onPolicyDeleted)}
- />
-
- );
- }}
-
+ >
+ executePolicyPrompt(name, onPolicyExecuted)}
+ disabled={Boolean(inProgress)}
+ />
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
+ {deletePolicyPrompt => {
+ return (
+
+ deletePolicyPrompt([name], onPolicyDeleted)}
+ />
+
+ );
+ }}
+
+
+
);
},
},
@@ -237,6 +289,19 @@ export const PolicyTable: React.FunctionComponent = ({
/>
+
+
+
+
+
),
box: {
@@ -268,6 +333,7 @@ export const PolicyTable: React.FunctionComponent = ({
return (
{
- return history.createHref({
- pathname: `${BASE_PATH}/repositories/${newRepositoryName}`,
- });
+ return linkToRepository(newRepositoryName);
};
const closeRepositoryDetails = () => {
@@ -116,9 +115,7 @@ export const RepositoryList: React.FunctionComponent {
}
return (
-
+ `index.${name}`)}>
{({ hasPrivileges, privilegesMissing }) =>
hasPrivileges ? (
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
index a5e886d0af077..bbec23d30622d 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
EuiDescriptionList,
@@ -112,6 +112,13 @@ export const TabSummary: React.SFC = ({ snapshotDetails }) => {
) : null;
+ // Reset indices list state when clicking through different snapshots
+ useEffect(() => {
+ return () => {
+ setIsShowingFullIndicesList(false);
+ };
+ }, []);
+
return (
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
index f6b716bcc18b6..7946d77ce8fab 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
@@ -7,15 +7,22 @@
import React, { Fragment, useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { parse } from 'querystring';
+import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui';
-import { EuiButton, EuiCallOut, EuiIcon, EuiLink, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
-
+import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants';
import { SectionError, SectionLoading } from '../../../components';
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
+import { WithPrivileges } from '../../../lib/authorization';
import { useAppDependencies } from '../../../index';
import { documentationLinksService } from '../../../services/documentation';
import { useLoadSnapshots } from '../../../services/http';
-import { linkToRepositories } from '../../../services/navigation';
+import {
+ linkToRepositories,
+ linkToAddRepository,
+ linkToPolicies,
+ linkToAddPolicy,
+ linkToSnapshot,
+} from '../../../services/navigation';
import { uiMetricService } from '../../../services/ui_metric';
import { SnapshotDetails } from './snapshot_details';
@@ -42,7 +49,7 @@ export const SnapshotList: React.FunctionComponent {
- return history.createHref({
- pathname: `${BASE_PATH}/snapshots/${encodeURIComponent(
- repositoryNameToOpen
- )}/${encodeURIComponent(snapshotIdToOpen)}`,
- });
+ return linkToSnapshot(repositoryNameToOpen, snapshotIdToOpen);
};
const closeSnapshotDetails = () => {
@@ -138,37 +141,22 @@ export const SnapshotList: React.FunctionComponent
}
body={
-
-
-
-
-
- ),
- }}
- />
-
-
-
- {' '}
-
-
-
-
+
+
+
+
+ ),
+ }}
+ />
+
}
/>
);
@@ -194,9 +182,7 @@ export const SnapshotList: React.FunctionComponent
}
body={
-
-
-
-
-
-
- {' '}
-
-
-
-
+ `cluster.${name}`)}>
+ {({ hasPrivileges }) =>
+ hasPrivileges ? (
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ {policies.length === 0 ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ ) : (
+
+
+
+
+
+
+ {' '}
+
+
+
+
+ )
+ }
+
}
data-test-subj="emptyPrompt"
/>
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/index.ts
index 1e89132252bec..ddd579a1a292f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/index.ts
@@ -8,3 +8,5 @@ export { SnapshotRestoreHome } from './home';
export { RepositoryAdd } from './repository_add';
export { RepositoryEdit } from './repository_edit';
export { RestoreSnapshot } from './restore_snapshot';
+export { PolicyAdd } from './policy_add';
+export { PolicyEdit } from './policy_edit';
diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/index.js b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/index.ts
similarity index 84%
rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/index.js
rename to x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/index.ts
index 764ff52dc73b9..45fa1353210cf 100644
--- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/cron_editor/index.js
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { CronEditor } from './cron_editor';
+export { PolicyAdd } from './policy_add';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
new file mode 100644
index 0000000000000..3f186dad142bb
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
@@ -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 React, { useEffect, useState } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+
+import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { SlmPolicyPayload } from '../../../../common/types';
+
+import { PolicyForm, SectionError, SectionLoading } from '../../components';
+import { useAppDependencies } from '../../index';
+import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants';
+import { breadcrumbService, docTitleService } from '../../services/navigation';
+import { addPolicy, useLoadIndicies } from '../../services/http';
+
+export const PolicyAdd: React.FunctionComponent = ({
+ history,
+ location: { pathname },
+}) => {
+ const {
+ core: {
+ i18n: { FormattedMessage },
+ },
+ } = useAppDependencies();
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+
+ const {
+ error: errorLoadingIndices,
+ isLoading: isLoadingIndices,
+ data: { indices } = {
+ indices: [],
+ },
+ } = useLoadIndicies();
+
+ // Set breadcrumb and page title
+ useEffect(() => {
+ breadcrumbService.setBreadcrumbs('policyAdd');
+ docTitleService.setTitle('policyAdd');
+ }, []);
+
+ const onSave = async (newPolicy: SlmPolicyPayload) => {
+ setIsSaving(true);
+ setSaveError(null);
+ const { name } = newPolicy;
+ const { error } = await addPolicy(newPolicy);
+ setIsSaving(false);
+ if (error) {
+ setSaveError(error);
+ } else {
+ history.push(`${BASE_PATH}/policies/${name}`);
+ }
+ };
+
+ const onCancel = () => {
+ history.push(`${BASE_PATH}/policies`);
+ };
+
+ const emptyPolicy: SlmPolicyPayload = {
+ name: '',
+ snapshotName: '',
+ schedule: DEFAULT_POLICY_SCHEDULE,
+ repository: '',
+ config: {},
+ };
+
+ const renderSaveError = () => {
+ return saveError ? (
+
+ }
+ error={saveError}
+ data-test-subj="savePolicyApiError"
+ />
+ ) : null;
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {isLoadingIndices ? (
+
+
+
+ ) : errorLoadingIndices ? (
+
+ }
+ error={errorLoadingIndices}
+ />
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/index.ts
new file mode 100644
index 0000000000000..68414d0ccf506
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/index.ts
@@ -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 { PolicyEdit } from './policy_edit';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
new file mode 100644
index 0000000000000..4ada745062c6f
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
@@ -0,0 +1,210 @@
+/*
+ * 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, { useEffect, useState, Fragment } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+
+import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { SlmPolicyPayload } from '../../../../common/types';
+
+import { SectionError, SectionLoading, PolicyForm } from '../../components';
+import { BASE_PATH } from '../../constants';
+import { useAppDependencies } from '../../index';
+import { breadcrumbService, docTitleService } from '../../services/navigation';
+import { editPolicy, useLoadPolicy, useLoadIndicies } from '../../services/http';
+
+interface MatchParams {
+ name: string;
+}
+
+export const PolicyEdit: React.FunctionComponent> = ({
+ match: {
+ params: { name },
+ },
+ history,
+ location: { pathname },
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+
+ // Set breadcrumb and page title
+ useEffect(() => {
+ breadcrumbService.setBreadcrumbs('policyEdit');
+ docTitleService.setTitle('policyEdit');
+ }, []);
+
+ // Policy state with default empty policy
+ const [policy, setPolicy] = useState({
+ name: '',
+ snapshotName: '',
+ schedule: '',
+ repository: '',
+ config: {},
+ });
+
+ const {
+ error: errorLoadingIndices,
+ isLoading: isLoadingIndices,
+ data: { indices } = {
+ indices: [],
+ },
+ } = useLoadIndicies();
+
+ // Load policy
+ const { error: errorLoadingPolicy, isLoading: isLoadingPolicy, data: policyData } = useLoadPolicy(
+ name
+ );
+
+ // Update policy state when data is loaded
+ useEffect(() => {
+ if (policyData && policyData.policy) {
+ setPolicy(policyData.policy);
+ }
+ }, [policyData]);
+
+ // Saving policy states
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+
+ // Save policy
+ const onSave = async (editedPolicy: SlmPolicyPayload) => {
+ setIsSaving(true);
+ setSaveError(null);
+ const { error } = await editPolicy(editedPolicy);
+ setIsSaving(false);
+ if (error) {
+ setSaveError(error);
+ } else {
+ history.push(`${BASE_PATH}/policies/${name}`);
+ }
+ };
+
+ const onCancel = () => {
+ history.push(`${BASE_PATH}/policies/${name}`);
+ };
+
+ const renderLoading = () => {
+ return errorLoadingPolicy ? (
+
+
+
+ ) : (
+
+
+
+ );
+ };
+
+ const renderError = () => {
+ if (errorLoadingPolicy) {
+ const notFound = errorLoadingPolicy.status === 404;
+ const errorObject = notFound
+ ? {
+ data: {
+ error: i18n.translate('xpack.snapshotRestore.editPolicy.policyNotFoundErrorMessage', {
+ defaultMessage: `The policy '{name}' does not exist.`,
+ values: {
+ name,
+ },
+ }),
+ },
+ }
+ : errorLoadingPolicy;
+ return (
+
+ }
+ error={errorObject}
+ />
+ );
+ }
+
+ if (errorLoadingIndices) {
+ return (
+
+ }
+ error={errorLoadingIndices}
+ />
+ );
+ }
+ };
+
+ const renderSaveError = () => {
+ return saveError ? (
+
+ }
+ error={saveError}
+ />
+ ) : null;
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
+ const renderContent = () => {
+ if (isLoadingPolicy || isLoadingIndices) {
+ return renderLoading();
+ }
+ if (errorLoadingPolicy || errorLoadingIndices) {
+ return renderError();
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {renderContent()}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx
index 28de033bd2d00..b4a76ff4329cf 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx
@@ -5,6 +5,7 @@
*/
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
+import { parse } from 'querystring';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
import { Repository, EmptyRepository } from '../../../../common/types';
@@ -12,10 +13,13 @@ import { Repository, EmptyRepository } from '../../../../common/types';
import { RepositoryForm, SectionError } from '../../components';
import { BASE_PATH, Section } from '../../constants';
import { useAppDependencies } from '../../index';
-import { breadcrumbService } from '../../services/navigation';
+import { breadcrumbService, docTitleService } from '../../services/navigation';
import { addRepository } from '../../services/http';
-export const RepositoryAdd: React.FunctionComponent = ({ history }) => {
+export const RepositoryAdd: React.FunctionComponent = ({
+ history,
+ location: { search },
+}) => {
const {
core: {
i18n: { FormattedMessage },
@@ -25,9 +29,10 @@ export const RepositoryAdd: React.FunctionComponent = ({ hi
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
- // Set breadcrumb
+ // Set breadcrumb and page title
useEffect(() => {
breadcrumbService.setBreadcrumbs('repositoryAdd');
+ docTitleService.setTitle('repositoryAdd');
}, []);
const onSave = async (newRepository: Repository | EmptyRepository) => {
@@ -39,7 +44,8 @@ export const RepositoryAdd: React.FunctionComponent = ({ hi
if (error) {
setSaveError(error);
} else {
- history.push(`${BASE_PATH}/${section}/${name}`);
+ const { redirect } = parse(search.replace(/^\?/, ''));
+ history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`);
}
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx
index 1c6b4eaba9d77..8544ea8f5ef1a 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx
@@ -12,7 +12,7 @@ import { Repository, EmptyRepository } from '../../../../common/types';
import { RepositoryForm, SectionError, SectionLoading } from '../../components';
import { BASE_PATH, Section } from '../../constants';
import { useAppDependencies } from '../../index';
-import { breadcrumbService } from '../../services/navigation';
+import { breadcrumbService, docTitleService } from '../../services/navigation';
import { editRepository, useLoadRepository } from '../../services/http';
interface MatchParams {
@@ -31,9 +31,10 @@ export const RepositoryEdit: React.FunctionComponent {
breadcrumbService.setBreadcrumbs('repositoryEdit');
+ docTitleService.setTitle('repositoryEdit');
}, []);
// Repository state with default empty repository
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/restore_snapshot/restore_snapshot.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/restore_snapshot/restore_snapshot.tsx
index baa46855184c3..53956cd007633 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/restore_snapshot/restore_snapshot.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/restore_snapshot/restore_snapshot.tsx
@@ -11,7 +11,7 @@ import { SnapshotDetails, RestoreSettings } from '../../../../common/types';
import { BASE_PATH } from '../../constants';
import { SectionError, SectionLoading, RestoreSnapshotForm } from '../../components';
import { useAppDependencies } from '../../index';
-import { breadcrumbService } from '../../services/navigation';
+import { breadcrumbService, docTitleService } from '../../services/navigation';
import { useLoadSnapshot, executeRestore } from '../../services/http';
interface MatchParams {
@@ -30,9 +30,10 @@ export const RestoreSnapshot: React.FunctionComponent {
breadcrumbService.setBreadcrumbs('restoreSnapshot');
+ docTitleService.setTitle('restoreSnapshot');
}, []);
// Snapshot details state with default empty snapshot
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
index 324f4d026c0b9..219292e7b0813 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
@@ -10,10 +10,16 @@ import { REPOSITORY_DOC_PATHS } from '../../constants';
class DocumentationLinksService {
private esDocBasePath: string = '';
private esPluginDocBasePath: string = '';
+ private esStackOverviewDocBasePath: string = '';
- public init(esDocBasePath: string, esPluginDocBasePath: string): void {
+ public init(
+ esDocBasePath: string,
+ esPluginDocBasePath: string,
+ esStackOverviewDocBasePath: string
+ ): void {
this.esDocBasePath = esDocBasePath;
this.esPluginDocBasePath = esPluginDocBasePath;
+ this.esStackOverviewDocBasePath = esStackOverviewDocBasePath;
}
public getRepositoryPluginDocUrl() {
@@ -42,7 +48,7 @@ class DocumentationLinksService {
}
public getSnapshotDocUrl() {
- return `${this.esDocBasePath}/modules-snapshots.html#_snapshot`;
+ return `${this.esDocBasePath}/modules-snapshots.html#snapshots-take-snapshot`;
}
public getRestoreDocUrl() {
@@ -56,6 +62,18 @@ class DocumentationLinksService {
public getIndexSettingsUrl() {
return `${this.esDocBasePath}/index-modules.html`;
}
+
+ public getDateMathIndexNamesUrl() {
+ return `${this.esDocBasePath}/date-math-index-names.html`;
+ }
+
+ public getSlmUrl() {
+ return `${this.esDocBasePath}/slm-api-put.html`;
+ }
+
+ public getCronUrl() {
+ return `${this.esStackOverviewDocBasePath}/trigger-schedule.html#schedule-cron`;
+ }
}
export const documentationLinksService = new DocumentationLinksService();
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts
index 6a2c9c685a01f..f8266833ec3e6 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts
@@ -4,8 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { API_BASE_PATH } from '../../../../common/constants';
-import { SlmPolicy } from '../../../../common/types';
-import { UIM_POLICY_EXECUTE, UIM_POLICY_DELETE, UIM_POLICY_DELETE_MANY } from '../../constants';
+import { SlmPolicy, SlmPolicyPayload } from '../../../../common/types';
+import {
+ UIM_POLICY_EXECUTE,
+ UIM_POLICY_DELETE,
+ UIM_POLICY_DELETE_MANY,
+ UIM_POLICY_CREATE,
+ UIM_POLICY_UPDATE,
+} from '../../constants';
import { uiMetricService } from '../ui_metric';
import { httpService } from './http';
import { useRequest, sendRequest } from './use_request';
@@ -24,6 +30,13 @@ export const useLoadPolicy = (name: SlmPolicy['name']) => {
});
};
+export const useLoadIndicies = () => {
+ return useRequest({
+ path: httpService.addBasePath(`${API_BASE_PATH}policies/indices`),
+ method: 'get',
+ });
+};
+
export const executePolicy = async (name: SlmPolicy['name']) => {
const result = sendRequest({
path: httpService.addBasePath(`${API_BASE_PATH}policy/${encodeURIComponent(name)}/run`),
@@ -47,3 +60,29 @@ export const deletePolicies = async (names: Array) => {
trackUiMetric(names.length > 1 ? UIM_POLICY_DELETE_MANY : UIM_POLICY_DELETE);
return result;
};
+
+export const addPolicy = async (newPolicy: SlmPolicyPayload) => {
+ const result = sendRequest({
+ path: httpService.addBasePath(`${API_BASE_PATH}policies`),
+ method: 'put',
+ body: newPolicy,
+ });
+
+ const { trackUiMetric } = uiMetricService;
+ trackUiMetric(UIM_POLICY_CREATE);
+ return result;
+};
+
+export const editPolicy = async (editedPolicy: SlmPolicyPayload) => {
+ const result = await sendRequest({
+ path: httpService.addBasePath(
+ `${API_BASE_PATH}policies/${encodeURIComponent(editedPolicy.name)}`
+ ),
+ method: 'put',
+ body: editedPolicy,
+ });
+
+ const { trackUiMetric } = uiMetricService;
+ trackUiMetric(UIM_POLICY_UPDATE);
+ return result;
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts
index fa9b886fa55b8..23d3f215d058c 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts
@@ -4,50 +4,128 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { BASE_PATH } from '../../constants';
import { textService } from '../text';
+import {
+ linkToHome,
+ linkToSnapshots,
+ linkToRepositories,
+ linkToPolicies,
+ linkToRestoreStatus,
+} from './';
class BreadcrumbService {
private chrome: any;
- private breadcrumbs: any = {
- management: {},
- home: {},
- repositoryAdd: {},
- repositoryEdit: {},
- restoreSnapshot: {},
+ private breadcrumbs: {
+ [key: string]: Array<{
+ text: string;
+ href?: string;
+ }>;
+ } = {
+ management: [],
+ home: [],
+ snapshots: [],
+ repositories: [],
+ policies: [],
+ restore_status: [],
+ repositoryAdd: [],
+ repositoryEdit: [],
+ restoreSnapshot: [],
+ policyAdd: [],
+ policyEdit: [],
};
public init(chrome: any, managementBreadcrumb: any): void {
this.chrome = chrome;
- this.breadcrumbs.management = managementBreadcrumb;
- this.breadcrumbs.home = {
- text: textService.breadcrumbs.home,
- href: `#${BASE_PATH}`,
- };
- this.breadcrumbs.repositoryAdd = {
- text: textService.breadcrumbs.repositoryAdd,
- };
- this.breadcrumbs.repositoryEdit = {
- text: textService.breadcrumbs.repositoryEdit,
- };
- this.breadcrumbs.restoreSnapshot = {
- text: textService.breadcrumbs.restoreSnapshot,
- };
+ this.breadcrumbs.management = [managementBreadcrumb];
+
+ // Home and sections
+ this.breadcrumbs.home = [
+ ...this.breadcrumbs.management,
+ {
+ text: textService.breadcrumbs.home,
+ href: linkToHome(),
+ },
+ ];
+ this.breadcrumbs.snapshots = [
+ ...this.breadcrumbs.home,
+ {
+ text: textService.breadcrumbs.snapshots,
+ href: linkToSnapshots(),
+ },
+ ];
+ this.breadcrumbs.repositories = [
+ ...this.breadcrumbs.home,
+ {
+ text: textService.breadcrumbs.repositories,
+ href: linkToRepositories(),
+ },
+ ];
+ this.breadcrumbs.policies = [
+ ...this.breadcrumbs.home,
+ {
+ text: textService.breadcrumbs.policies,
+ href: linkToPolicies(),
+ },
+ ];
+ this.breadcrumbs.restore_status = [
+ ...this.breadcrumbs.home,
+ {
+ text: textService.breadcrumbs.restore_status,
+ href: linkToRestoreStatus(),
+ },
+ ];
+
+ // Inner pages
+ this.breadcrumbs.repositoryAdd = [
+ ...this.breadcrumbs.repositories,
+ {
+ text: textService.breadcrumbs.repositoryAdd,
+ },
+ ];
+ this.breadcrumbs.repositoryEdit = [
+ ...this.breadcrumbs.repositories,
+ {
+ text: textService.breadcrumbs.repositoryEdit,
+ },
+ ];
+ this.breadcrumbs.restoreSnapshot = [
+ ...this.breadcrumbs.snapshots,
+ {
+ text: textService.breadcrumbs.restoreSnapshot,
+ },
+ ];
+ this.breadcrumbs.policyAdd = [
+ ...this.breadcrumbs.policies,
+ {
+ text: textService.breadcrumbs.policyAdd,
+ },
+ ];
+ this.breadcrumbs.policyEdit = [
+ ...this.breadcrumbs.policies,
+ {
+ text: textService.breadcrumbs.policyEdit,
+ },
+ ];
}
public setBreadcrumbs(type: string): void {
- if (!this.breadcrumbs[type]) {
- return;
- }
- if (type === 'home') {
- this.chrome.breadcrumbs.set([this.breadcrumbs.management, this.breadcrumbs.home]);
- } else {
- this.chrome.breadcrumbs.set([
- this.breadcrumbs.management,
- this.breadcrumbs.home,
- this.breadcrumbs[type],
- ]);
- }
+ const newBreadcrumbs = this.breadcrumbs[type]
+ ? [...this.breadcrumbs[type]]
+ : [...this.breadcrumbs.home];
+
+ // Pop off last breadcrumb
+ const lastBreadcrumb = newBreadcrumbs.pop() as {
+ text: string;
+ href?: string;
+ };
+
+ // Put last breadcrumb back without href
+ newBreadcrumbs.push({
+ ...lastBreadcrumb,
+ href: undefined,
+ });
+
+ this.chrome.breadcrumbs.set(newBreadcrumbs);
}
}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/doc_title.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/doc_title.ts
new file mode 100644
index 0000000000000..a42d09f2a2f45
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/doc_title.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { textService } from '../text';
+
+class DocTitleService {
+ private changeDocTitle: any = () => {};
+
+ public init(changeDocTitle: any): void {
+ this.changeDocTitle = changeDocTitle;
+ }
+
+ public setTitle(page?: string): void {
+ if (!page || page === 'home') {
+ this.changeDocTitle(`${textService.breadcrumbs.home}`);
+ } else if (textService.breadcrumbs[page]) {
+ this.changeDocTitle(`${textService.breadcrumbs[page]} - ${textService.breadcrumbs.home}`);
+ }
+ }
+}
+
+export const docTitleService = new DocTitleService();
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/index.ts
index f1e3c537c5d70..badb47600329d 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/index.ts
@@ -5,4 +5,5 @@
*/
export { breadcrumbService } from './breadcrumb';
+export { docTitleService } from './doc_title';
export * from './links';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/links.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/links.ts
index 9f8426e84e214..6f95000726106 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/links.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/navigation/links.ts
@@ -6,6 +6,10 @@
import { BASE_PATH } from '../../constants';
+export function linkToHome() {
+ return `#${BASE_PATH}`;
+}
+
export function linkToRepositories() {
return `#${BASE_PATH}/repositories`;
}
@@ -18,8 +22,10 @@ export function linkToEditRepository(repositoryName: string) {
return `#${BASE_PATH}/edit_repository/${encodeURIComponent(repositoryName)}`;
}
-export function linkToAddRepository() {
- return `#${BASE_PATH}/add_repository`;
+export function linkToAddRepository(redirect?: string) {
+ return `#${BASE_PATH}/add_repository${
+ redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''
+ }`;
}
export function linkToSnapshots(repositoryName?: string, policyName?: string) {
@@ -44,6 +50,22 @@ export function linkToRestoreSnapshot(repositoryName: string, snapshotName: stri
)}`;
}
+export function linkToPolicies() {
+ return `#${BASE_PATH}/policies`;
+}
+
export function linkToPolicy(policyName: string) {
return `#${BASE_PATH}/policies/${encodeURIComponent(policyName)}`;
}
+
+export function linkToEditPolicy(policyName: string) {
+ return `#${BASE_PATH}/edit_policy/${encodeURIComponent(policyName)}`;
+}
+
+export function linkToAddPolicy() {
+ return `#${BASE_PATH}/add_policy`;
+}
+
+export function linkToRestoreStatus() {
+ return `#${BASE_PATH}/restore_status`;
+}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
index 50e6555e9bce4..ec92250373a05 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
@@ -51,6 +51,18 @@ class TextService {
home: i18n.translate('xpack.snapshotRestore.home.breadcrumbTitle', {
defaultMessage: 'Snapshot and Restore',
}),
+ snapshots: i18n.translate('xpack.snapshotRestore.snapshots.breadcrumbTitle', {
+ defaultMessage: 'Snapshots',
+ }),
+ repositories: i18n.translate('xpack.snapshotRestore.repositories.breadcrumbTitle', {
+ defaultMessage: 'Repositories',
+ }),
+ policies: i18n.translate('xpack.snapshotRestore.policies.breadcrumbTitle', {
+ defaultMessage: 'Policies',
+ }),
+ restore_status: i18n.translate('xpack.snapshotRestore.restoreStatus.breadcrumbTitle', {
+ defaultMessage: 'Restore Status',
+ }),
repositoryAdd: i18n.translate('xpack.snapshotRestore.addRepository.breadcrumbTitle', {
defaultMessage: 'Add repository',
}),
@@ -60,6 +72,12 @@ class TextService {
restoreSnapshot: i18n.translate('xpack.snapshotRestore.restoreSnapshot.breadcrumbTitle', {
defaultMessage: 'Restore snapshot',
}),
+ policyAdd: i18n.translate('xpack.snapshotRestore.addPolicy.breadcrumbTitle', {
+ defaultMessage: 'Add policy',
+ }),
+ policyEdit: i18n.translate('xpack.snapshotRestore.editPolicy.breadcrumbTitle', {
+ defaultMessage: 'Edit policy',
+ }),
};
}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/index.ts
index f987d432f02f6..7fd755497eec6 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/index.ts
@@ -11,3 +11,5 @@ export {
} from './validate_repository';
export { RestoreValidation, validateRestore } from './validate_restore';
+
+export { PolicyValidation, validatePolicy } from './validate_policy';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
new file mode 100644
index 0000000000000..53c62da97bdac
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 { SlmPolicyPayload } from '../../../../common/types';
+import { textService } from '../text';
+
+export interface PolicyValidation {
+ isValid: boolean;
+ errors: { [key: string]: React.ReactNode[] };
+}
+
+const isStringEmpty = (str: string | null): boolean => {
+ return str ? !Boolean(str.trim()) : true;
+};
+
+export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
+ const i18n = textService.i18n;
+
+ const { name, snapshotName, schedule, repository, config } = policy;
+
+ const validation: PolicyValidation = {
+ isValid: true,
+ errors: {
+ name: [],
+ snapshotName: [],
+ schedule: [],
+ repository: [],
+ indices: [],
+ },
+ };
+
+ if (isStringEmpty(name)) {
+ validation.errors.name.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.nameRequiredError', {
+ defaultMessage: 'Policy name is required.',
+ })
+ );
+ }
+
+ if (isStringEmpty(snapshotName)) {
+ validation.errors.snapshotName.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.snapshotNameRequiredError', {
+ defaultMessage: 'Snapshot name is required.',
+ })
+ );
+ }
+
+ if (isStringEmpty(schedule)) {
+ validation.errors.schedule.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.scheduleRequiredError', {
+ defaultMessage: 'Schedule is required.',
+ })
+ );
+ }
+
+ if (isStringEmpty(repository)) {
+ validation.errors.repository.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.repositoryRequiredError', {
+ defaultMessage: 'Repository is required.',
+ })
+ );
+ }
+
+ if (config && typeof config.indices === 'string' && config.indices.trim().length === 0) {
+ validation.errors.indices.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredError', {
+ defaultMessage: 'At least one index pattern is required.',
+ })
+ );
+ }
+
+ if (config && Array.isArray(config.indices) && config.indices.length === 0) {
+ validation.errors.indices.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredError', {
+ defaultMessage: 'You must select at least one index.',
+ })
+ );
+ }
+
+ // Remove fields with no errors
+ validation.errors = Object.entries(validation.errors)
+ .filter(([key, value]) => value.length > 0)
+ .reduce((errs: PolicyValidation['errors'], [key, value]) => {
+ errs[key] = value;
+ return errs;
+ }, {});
+
+ // Set overall validations status
+ if (Object.keys(validation.errors).length > 0) {
+ validation.isValid = false;
+ }
+
+ return validation;
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
index f590237bec737..cd6d7233722bd 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
@@ -11,7 +11,7 @@ import { AppCore, AppPlugins } from './app/types';
import template from './index.html';
import { Core, Plugins } from './shim';
-import { breadcrumbService } from './app/services/navigation';
+import { breadcrumbService, docTitleService } from './app/services/navigation';
import { documentationLinksService } from './app/services/documentation';
import { httpService } from './app/services/http';
import { textService } from './app/services/text';
@@ -21,7 +21,7 @@ const REACT_ROOT_ID = 'snapshotRestoreReactRoot';
export class Plugin {
public start(core: Core, plugins: Plugins): void {
- const { i18n, routing, http, chrome, notification, documentation } = core;
+ const { i18n, routing, http, chrome, notification, documentation, docTitle } = core;
const { management, uiMetric } = plugins;
// Register management section
@@ -38,8 +38,13 @@ export class Plugin {
// Initialize services
textService.init(i18n);
breadcrumbService.init(chrome, management.constants.BREADCRUMB);
- documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath);
uiMetricService.init(uiMetric.createUiStatsReporter);
+ documentationLinksService.init(
+ documentation.esDocBasePath,
+ documentation.esPluginDocBasePath,
+ documentation.esStackOverviewDocBasePath
+ );
+ docTitleService.init(docTitle.change);
const unmountReactApp = (): void => {
const elem = document.getElementById(REACT_ROOT_ID);
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/legacy/plugins/snapshot_restore/public/shared_imports.ts
index 3d93b882733ab..c79eaa08de95f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/shared_imports.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/shared_imports.ts
@@ -11,3 +11,8 @@ export {
sendRequest,
useRequest,
} from '../../../../../src/plugins/es_ui_shared/public/request';
+
+export {
+ CronEditor,
+ DAY,
+} from '../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
index 77604f90fd570..9c9d2d7d3ea86 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
@@ -12,6 +12,7 @@ import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links';
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { fatalError, toastNotifications } from 'ui/notify';
import routes from 'ui/routes';
+import { docTitle } from 'ui/doc_title/doc_title';
import { HashRouter } from 'react-router-dom';
@@ -52,6 +53,10 @@ export interface Core extends AppCore {
documentation: {
esDocBasePath: string;
esPluginDocBasePath: string;
+ esStackOverviewDocBasePath: string;
+ };
+ docTitle: {
+ change: typeof docTitle.change;
};
}
@@ -108,6 +113,10 @@ export function createShim(): { core: Core; plugins: Plugins } {
documentation: {
esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`,
esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`,
+ esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`,
+ },
+ docTitle: {
+ change: docTitle.change,
},
},
plugins: {
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts
index c37cc51f67eb0..79196f9bbe385 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts
@@ -60,4 +60,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
],
method: 'PUT',
});
+
+ slm.updatePolicy = ca({
+ urls: [
+ {
+ fmt: '/_slm/policy/<%=name%>',
+ req: {
+ name: {
+ type: 'string',
+ },
+ },
+ },
+ ],
+ method: 'PUT',
+ });
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts b/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts
index b0d65ff06d80e..6e54f997209ab 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts
@@ -9,7 +9,5 @@ export {
serializeRepositorySettings,
} from './repository_serialization';
export { cleanSettings } from './clean_settings';
-export { deserializeSnapshotDetails, deserializeSnapshotConfig } from './snapshot_serialization';
-export { deserializeRestoreShard } from './restore_serialization';
export { getManagedRepositoryName } from './get_managed_repository_name';
-export { deserializePolicy } from './policy_serialization';
+export { deserializeRestoreShard } from './restore_serialization';
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts
index c97317858f98a..6c7ad0ae30387 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts
@@ -8,6 +8,7 @@ import { wrapCustomError } from '../../../../../server/lib/create_router/error_w
import {
APP_REQUIRED_CLUSTER_PRIVILEGES,
APP_RESTORE_INDEX_PRIVILEGES,
+ APP_SLM_CLUSTER_PRIVILEGES,
} from '../../../common/constants';
// NOTE: now we import it from our "public" folder, but when the Authorisation lib
// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder
@@ -65,7 +66,7 @@ export const getPrivilegesHandler: RouterRouteHandler = async (
path: '/_security/user/_has_privileges',
method: 'POST',
body: {
- cluster: APP_REQUIRED_CLUSTER_PRIVILEGES,
+ cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES],
},
}
);
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts
index f2335d4f78dd9..52e6449559bcc 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts
@@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request, ResponseToolkit } from 'hapi';
-import { getAllHandler, getOneHandler, executeHandler, deleteHandler } from './policy';
+import {
+ getAllHandler,
+ getOneHandler,
+ executeHandler,
+ deleteHandler,
+ createHandler,
+ updateHandler,
+ getIndicesHandler,
+} from './policy';
describe('[Snapshot and Restore API Routes] Restore', () => {
const mockRequest = {} as Request;
@@ -209,4 +217,110 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
).resolves.toEqual(expectedResponse);
});
});
+
+ describe('createHandler()', () => {
+ const name = 'fooPolicy';
+ const mockCreateRequest = ({
+ payload: {
+ name,
+ },
+ } as unknown) as Request;
+
+ it('should return successful ES response', async () => {
+ const mockEsResponse = { acknowledged: true };
+ const callWithRequest = jest
+ .fn()
+ .mockReturnValueOnce({})
+ .mockReturnValueOnce(mockEsResponse);
+ const expectedResponse = { ...mockEsResponse };
+ await expect(
+ createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).resolves.toEqual(expectedResponse);
+ });
+
+ it('should return error if policy with the same name already exists', async () => {
+ const mockEsResponse = { [name]: {} };
+ const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
+ await expect(
+ createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).rejects.toThrow();
+ });
+
+ it('should throw if ES error', async () => {
+ const callWithRequest = jest
+ .fn()
+ .mockReturnValueOnce({})
+ .mockRejectedValueOnce(new Error());
+ await expect(
+ createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).rejects.toThrow();
+ });
+ });
+
+ describe('updateHandler()', () => {
+ const name = 'fooPolicy';
+ const mockCreateRequest = ({
+ params: {
+ name,
+ },
+ payload: {
+ name,
+ },
+ } as unknown) as Request;
+
+ it('should return successful ES response', async () => {
+ const mockEsResponse = { acknowledged: true };
+ const callWithRequest = jest
+ .fn()
+ .mockReturnValueOnce({ [name]: {} })
+ .mockReturnValueOnce(mockEsResponse);
+ const expectedResponse = { ...mockEsResponse };
+ await expect(
+ updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).resolves.toEqual(expectedResponse);
+ });
+
+ it('should throw if ES error', async () => {
+ const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
+ await expect(
+ updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).rejects.toThrow();
+ });
+ });
+
+ describe('getIndicesHandler()', () => {
+ it('should arrify and sort index names returned from ES', async () => {
+ const mockEsResponse = [
+ {
+ index: 'fooIndex',
+ },
+ {
+ index: 'barIndex',
+ },
+ ];
+ const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
+ const expectedResponse = {
+ indices: ['barIndex', 'fooIndex'],
+ };
+ await expect(
+ getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
+ ).resolves.toEqual(expectedResponse);
+ });
+
+ it('should return empty array if no indices returned from ES', async () => {
+ const mockEsResponse: any[] = [];
+ const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
+ const expectedResponse = { indices: [] };
+ await expect(
+ getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
+ ).resolves.toEqual(expectedResponse);
+ });
+
+ it('should throw if ES error', async () => {
+ const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
+ await expect(
+ getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
+ ).rejects.toThrow();
+ });
+ });
});
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
index 28b75b706bcad..ed16a44bccdc6 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
@@ -8,14 +8,17 @@ import {
wrapCustomError,
wrapEsError,
} from '../../../../../server/lib/create_router/error_wrappers';
-import { SlmPolicyEs, SlmPolicy } from '../../../common/types';
-import { deserializePolicy } from '../../lib';
+import { SlmPolicyEs, SlmPolicy, SlmPolicyPayload } from '../../../common/types';
+import { deserializePolicy, serializePolicy } from '../../../common/lib';
export function registerPolicyRoutes(router: Router) {
router.get('policies', getAllHandler);
router.get('policy/{name}', getOneHandler);
router.post('policy/{name}/run', executeHandler);
router.delete('policies/{names}', deleteHandler);
+ router.put('policies', createHandler);
+ router.put('policies/{name}', updateHandler);
+ router.get('policies/indices', getIndicesHandler);
}
export const getAllHandler: RouterRouteHandler = async (
@@ -96,3 +99,65 @@ export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) =>
return response;
};
+
+export const createHandler: RouterRouteHandler = async (req, callWithRequest) => {
+ const policy = req.payload as SlmPolicyPayload;
+ const { name } = policy;
+ const conflictError = wrapCustomError(
+ new Error('There is already a policy with that name.'),
+ 409
+ );
+
+ // Check that policy with the same name doesn't already exist
+ try {
+ const policyByName = await callWithRequest('slm.policy', { name });
+ if (policyByName[name]) {
+ throw conflictError;
+ }
+ } catch (e) {
+ // Rethrow conflict error but silently swallow all others
+ if (e === conflictError) {
+ throw e;
+ }
+ }
+
+ // Otherwise create new policy
+ return await callWithRequest('slm.updatePolicy', {
+ name,
+ body: serializePolicy(policy),
+ });
+};
+
+export const updateHandler: RouterRouteHandler = async (req, callWithRequest) => {
+ const { name } = req.params;
+ const policy = req.payload as SlmPolicyPayload;
+
+ // Check that policy with the given name exists
+ // If it doesn't exist, 404 will be thrown by ES and will be returned
+ await callWithRequest('slm.policy', { name });
+
+ // Otherwise update policy
+ return await callWithRequest('slm.updatePolicy', {
+ name,
+ body: serializePolicy(policy),
+ });
+};
+
+export const getIndicesHandler: RouterRouteHandler = async (
+ req,
+ callWithRequest
+): Promise<{
+ indices: string[];
+}> => {
+ // Get indices
+ const indices: Array<{
+ index: string;
+ }> = await callWithRequest('cat.indices', {
+ format: 'json',
+ h: 'index',
+ });
+
+ return {
+ indices: indices.map(({ index }) => index).sort(),
+ };
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
index ba422367415c0..5abadaab59d7e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
@@ -55,6 +55,10 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
const mockRequest = {} as Request;
test('combines snapshots and their repositories returned from ES', async () => {
+ const mockSnapshotGetPolicyEsResponse = {
+ fooPolicy: {},
+ };
+
const mockSnapshotGetRepositoryEsResponse = {
fooRepository: {},
barRepository: {},
@@ -78,6 +82,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
const callWithRequest = jest
.fn()
+ .mockReturnValueOnce(mockSnapshotGetPolicyEsResponse)
.mockReturnValueOnce(mockSnapshotGetRepositoryEsResponse)
.mockReturnValueOnce(mockGetSnapshotsFooResponse)
.mockReturnValueOnce(mockGetSnapshotsBarResponse);
@@ -85,6 +90,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
const expectedResponse = {
errors: {},
repositories: ['fooRepository', 'barRepository'],
+ policies: ['fooPolicy'],
snapshots: [
{
...defaultSnapshot,
@@ -106,12 +112,17 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
});
test('returns empty arrays if no snapshots returned from ES', async () => {
+ const mockSnapshotGetPolicyEsResponse = {};
const mockSnapshotGetRepositoryEsResponse = {};
- const callWithRequest = jest.fn().mockReturnValue(mockSnapshotGetRepositoryEsResponse);
+ const callWithRequest = jest
+ .fn()
+ .mockReturnValue(mockSnapshotGetPolicyEsResponse)
+ .mockReturnValue(mockSnapshotGetRepositoryEsResponse);
const expectedResponse = {
errors: [],
snapshots: [],
repositories: [],
+ policies: [],
};
const response = await getAllHandler(mockRequest, callWithRequest, mockResponseToolkit);
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
index 5933f1e47bc12..ec973d500f84f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
@@ -6,8 +6,9 @@
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
import { wrapEsError } from '../../../../../server/lib/create_router/error_wrappers';
import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types';
+import { deserializeSnapshotDetails } from '../../../common/lib';
import { Plugins } from '../../../shim';
-import { deserializeSnapshotDetails, getManagedRepositoryName } from '../../lib';
+import { getManagedRepositoryName } from '../../lib';
let callWithInternalUser: any;
@@ -24,10 +25,22 @@ export const getAllHandler: RouterRouteHandler = async (
): Promise<{
snapshots: SnapshotDetails[];
errors: any[];
+ policies: string[];
repositories: string[];
managedRepository?: string;
}> => {
const managedRepository = await getManagedRepositoryName(callWithInternalUser);
+ let policies: string[] = [];
+
+ // Attempt to retrieve policies
+ // This could fail if user doesn't have access to read SLM policies
+ try {
+ const policiesByName = await callWithRequest('slm.policies');
+ policies = Object.keys(policiesByName);
+ } catch (e) {
+ // Silently swallow error as policy names aren't required in UI
+ }
+
const repositoriesByName = await callWithRequest('snapshot.getRepository', {
repository: '_all',
});
@@ -35,7 +48,7 @@ export const getAllHandler: RouterRouteHandler = async (
const repositoryNames = Object.keys(repositoriesByName);
if (repositoryNames.length === 0) {
- return { snapshots: [], errors: [], repositories: [] };
+ return { snapshots: [], errors: [], repositories: [], policies };
}
const snapshots: SnapshotDetails[] = [];
@@ -70,6 +83,7 @@ export const getAllHandler: RouterRouteHandler = async (
return {
snapshots,
+ policies,
repositories,
errors,
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b43df00186bf2..f176e8c37ce5d 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8712,26 +8712,6 @@
"xpack.rollupJobs.createAction.jobIdAlreadyExistsErrorMessage": "ID「{jobConfigId}」のジョブが既に存在します。",
"xpack.rollupJobs.createBreadcrumbTitle": "作成",
"xpack.rollupJobs.createTitle": "ロールアップジョブを作成",
- "xpack.rollupJobs.cronEditor.cronDaily.fieldHour.textAtLabel": "時点で",
- "xpack.rollupJobs.cronEditor.cronDaily.fieldTimeLabel": "時間",
- "xpack.rollupJobs.cronEditor.cronHourly.fieldMinute.textAtLabel": "時点で",
- "xpack.rollupJobs.cronEditor.cronHourly.fieldTimeLabel": "分",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldDateLabel": "日付",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldHour.textAtLabel": "時点で",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldTimeLabel": "時間",
- "xpack.rollupJobs.cronEditor.cronMonthly.textOnTheLabel": "On the",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldDateLabel": "日",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldHour.textAtLabel": "時点で",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldTimeLabel": "時間",
- "xpack.rollupJobs.cronEditor.cronWeekly.textOnLabel": "オン",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldDate.textOnTheLabel": "On the",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldDateLabel": "日付",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldHour.textAtLabel": "時点で",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldMonth.textInLabel": "In",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldMonthLabel": "月",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldTimeLabel": "時間",
- "xpack.rollupJobs.cronEditor.fieldFrequencyLabel": "頻度",
- "xpack.rollupJobs.cronEditor.textEveryLabel": "毎",
"xpack.rollupJobs.deleteAction.errorTitle": "ロールアップジョブの削除中にエラーが発生",
"xpack.rollupJobs.deleteAction.successMultipleNotificationTitle": "{count} 件のロールアップジョブが削除されました",
"xpack.rollupJobs.deleteAction.successSingleNotificationTitle": "ロールアップジョブ「{jobId}」が削除されました",
@@ -8815,25 +8795,6 @@
"xpack.rollupJobs.rollupIndexPatternsTitle": "ロールアップインデックスパターンを有効にする",
"xpack.rollupJobs.startJobsAction.errorTitle": "ロールアップジョブの開始中にエラーが発生",
"xpack.rollupJobs.stopJobsAction.errorTitle": "ロールアップジョブの停止中にエラーが発生",
- "xpack.rollupJobs.util.day.friday": "金曜日",
- "xpack.rollupJobs.util.day.monday": "月曜日",
- "xpack.rollupJobs.util.day.saturday": "土曜日",
- "xpack.rollupJobs.util.day.sunday": "日曜日",
- "xpack.rollupJobs.util.day.thursday": "木曜日",
- "xpack.rollupJobs.util.day.tuesday": "火曜日",
- "xpack.rollupJobs.util.day.wednesday": "水曜日",
- "xpack.rollupJobs.util.month.april": "4 月",
- "xpack.rollupJobs.util.month.august": "8 月",
- "xpack.rollupJobs.util.month.december": "12 月",
- "xpack.rollupJobs.util.month.february": "2 月",
- "xpack.rollupJobs.util.month.january": "1 月",
- "xpack.rollupJobs.util.month.july": "7 月",
- "xpack.rollupJobs.util.month.june": "6 月",
- "xpack.rollupJobs.util.month.march": "3 月",
- "xpack.rollupJobs.util.month.may": "5 月",
- "xpack.rollupJobs.util.month.november": "11 月",
- "xpack.rollupJobs.util.month.october": "10 月",
- "xpack.rollupJobs.util.month.september": "9 月",
"xpack.searchProfiler.aggregationProfileTabTitle": "集約プロフィール",
"xpack.searchProfiler.basicLicenseTitle": "ベーシック",
"xpack.searchProfiler.formIndexLabel": "インデックス",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index a1d580fb5ad88..db1e85aacb385 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8855,26 +8855,6 @@
"xpack.rollupJobs.createAction.jobIdAlreadyExistsErrorMessage": "ID 为 “{jobConfigId}” 的作业已存在。",
"xpack.rollupJobs.createBreadcrumbTitle": "创建",
"xpack.rollupJobs.createTitle": "创建汇总/打包作业",
- "xpack.rollupJobs.cronEditor.cronDaily.fieldHour.textAtLabel": "在",
- "xpack.rollupJobs.cronEditor.cronDaily.fieldTimeLabel": "时间",
- "xpack.rollupJobs.cronEditor.cronHourly.fieldMinute.textAtLabel": "在",
- "xpack.rollupJobs.cronEditor.cronHourly.fieldTimeLabel": "分钟",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldDateLabel": "日期",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldHour.textAtLabel": "在",
- "xpack.rollupJobs.cronEditor.cronMonthly.fieldTimeLabel": "时间",
- "xpack.rollupJobs.cronEditor.cronMonthly.textOnTheLabel": "处于",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldDateLabel": "天",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldHour.textAtLabel": "在",
- "xpack.rollupJobs.cronEditor.cronWeekly.fieldTimeLabel": "时间",
- "xpack.rollupJobs.cronEditor.cronWeekly.textOnLabel": "开启",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldDate.textOnTheLabel": "处于",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldDateLabel": "日期",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldHour.textAtLabel": "在",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldMonth.textInLabel": "于",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldMonthLabel": "月",
- "xpack.rollupJobs.cronEditor.cronYearly.fieldTimeLabel": "时间",
- "xpack.rollupJobs.cronEditor.fieldFrequencyLabel": "频率",
- "xpack.rollupJobs.cronEditor.textEveryLabel": "所有",
"xpack.rollupJobs.deleteAction.errorTitle": "删除汇总/打包作业时出错",
"xpack.rollupJobs.deleteAction.successMultipleNotificationTitle": "已删除 {count} 个汇总/打包作业",
"xpack.rollupJobs.deleteAction.successSingleNotificationTitle": "已删除汇总/打包作业“{jobId}”",
@@ -8958,25 +8938,6 @@
"xpack.rollupJobs.rollupIndexPatternsTitle": "启用汇总索引模式",
"xpack.rollupJobs.startJobsAction.errorTitle": "启动汇总/打包作业时出错",
"xpack.rollupJobs.stopJobsAction.errorTitle": "停止汇总/打包作业时出错",
- "xpack.rollupJobs.util.day.friday": "星期五",
- "xpack.rollupJobs.util.day.monday": "星期一",
- "xpack.rollupJobs.util.day.saturday": "星期六",
- "xpack.rollupJobs.util.day.sunday": "星期日",
- "xpack.rollupJobs.util.day.thursday": "星期四",
- "xpack.rollupJobs.util.day.tuesday": "星期二",
- "xpack.rollupJobs.util.day.wednesday": "星期三",
- "xpack.rollupJobs.util.month.april": "四月",
- "xpack.rollupJobs.util.month.august": "八月",
- "xpack.rollupJobs.util.month.december": "十二月",
- "xpack.rollupJobs.util.month.february": "二月",
- "xpack.rollupJobs.util.month.january": "一月",
- "xpack.rollupJobs.util.month.july": "七月",
- "xpack.rollupJobs.util.month.june": "六月",
- "xpack.rollupJobs.util.month.march": "三月",
- "xpack.rollupJobs.util.month.may": "五月",
- "xpack.rollupJobs.util.month.november": "十一月",
- "xpack.rollupJobs.util.month.october": "十月",
- "xpack.rollupJobs.util.month.september": "九月",
"xpack.searchProfiler.aggregationProfileTabTitle": "聚合配置文件",
"xpack.searchProfiler.basicLicenseTitle": "基础级",
"xpack.searchProfiler.formIndexLabel": "索引",