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( + indices.map( + (index): Option => ({ + label: index, + checked: + isAllIndices || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode + typeof config.indices === 'string' || + (Array.isArray(config.indices) && config.indices.includes(index)) + ? 'on' + : undefined, + }) + ) + ); + + // State for using selectable indices list or custom patterns + // Users with more than 100 indices will probably want to use an index pattern to select + // them instead, so we'll default to showing them the index pattern input. + const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>( + typeof config.indices === 'string' || + (Array.isArray(config.indices) && config.indices.length > 100) + ? 'custom' + : 'list' + ); + + // State for custom patterns + const [indexPatterns, setIndexPatterns] = useState( + typeof config.indices === 'string' ? config.indices.split(',') : [] + ); + + const renderIndicesField = () => ( + +

+ +

+ + } + description={ + + } + idAria="indicesDescription" + fullWidth + > + + + + } + checked={isAllIndices} + onChange={e => { + const isChecked = e.target.checked; + setIsAllIndices(isChecked); + if (isChecked) { + updatePolicyConfig({ indices: undefined }); + } else { + updatePolicyConfig({ + indices: + selectIndicesMode === 'custom' + ? indexPatterns.join(',') + : [...(indicesSelection || [])], + }); + } + }} + /> + {isAllIndices ? null : ( + + + + + + + + { + setSelectIndicesMode('custom'); + updatePolicyConfig({ indices: indexPatterns.join(',') }); + }} + > + + + + + ) : ( + + + + + + { + setSelectIndicesMode('list'); + updatePolicyConfig({ indices: indicesSelection }); + }} + > + + + + + ) + } + helpText={ + selectIndicesMode === 'list' ? ( + 0 ? ( + { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesOptions.forEach((option: Option) => { + option.checked = undefined; + }); + updatePolicyConfig({ indices: [] }); + setIndicesSelection([]); + }} + > + + + ) : ( + { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesOptions.forEach((option: Option) => { + option.checked = 'on'; + }); + updatePolicyConfig({ indices: [...indices] }); + setIndicesSelection([...indices]); + }} + > + + + ), + }} + /> + ) : null + } + isInvalid={Boolean(errors.indices)} + error={errors.indices} + > + {selectIndicesMode === 'list' ? ( + { + const newSelectedIndices: string[] = []; + options.forEach(({ label, checked }) => { + if (checked === 'on') { + newSelectedIndices.push(label); + } + }); + setIndicesOptions(options); + updatePolicyConfig({ indices: newSelectedIndices }); + setIndicesSelection(newSelectedIndices); + }} + searchable + height={300} + > + {(list, search) => ( + + {search} + {list} + + )} + + ) : ( + ({ label: index }))} + placeholder={i18n.translate( + 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', + { + defaultMessage: 'Enter index patterns, i.e. logstash-*', + } + )} + selectedOptions={indexPatterns.map(pattern => ({ label: pattern }))} + onCreateOption={(pattern: string) => { + if (!pattern.trim().length) { + return; + } + const newPatterns = [...indexPatterns, pattern]; + setIndexPatterns(newPatterns); + updatePolicyConfig({ + indices: newPatterns.join(','), + }); + }} + onChange={(patterns: Array<{ label: string }>) => { + const newPatterns = patterns.map(({ label }) => label); + setIndexPatterns(newPatterns); + updatePolicyConfig({ + indices: newPatterns.join(','), + }); + }} + /> + )} + + + )} + + +
+ ); + + const renderIgnoreUnavailableField = () => ( + +

+ +

+ + } + description={ + + } + idAria="policyIgnoreUnavailableDescription" + fullWidth + > + + + } + checked={Boolean(config.ignoreUnavailable)} + onChange={e => { + updatePolicyConfig({ + ignoreUnavailable: e.target.checked, + }); + }} + /> + +
+ ); + + const renderPartialField = () => ( + +

+ +

+ + } + description={ + + } + idAria="policyPartialDescription" + fullWidth + > + + + } + checked={Boolean(config.partial)} + onChange={e => { + updatePolicyConfig({ + partial: e.target.checked, + }); + }} + /> + +
+ ); + + const renderIncludeGlobalStateField = () => ( + +

+ +

+ + } + description={ + + } + idAria="policyIncludeGlobalStateDescription" + fullWidth + > + + + } + checked={config.includeGlobalState === undefined || config.includeGlobalState} + onChange={e => { + updatePolicyConfig({ + includeGlobalState: e.target.checked, + }); + }} + /> + +
+ ); + return ( +
+ {/* Step title and doc link */} + + + +

+ +

+
+
+ + + + + + +
+ + + {renderIndicesField()} + {renderIgnoreUnavailableField()} + {renderPartialField()} + {renderIncludeGlobalStateField()} +
+ ); +}; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx index 509eeb0201825..f0991819f957f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx @@ -155,7 +155,8 @@ export const RepositoryDeleteProvider: React.FunctionComponent = ({ child

) : ( @@ -174,7 +175,8 @@ export const RepositoryDeleteProvider: React.FunctionComponent = ({ child

diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/restore_snapshot_form/steps/step_logistics.tsx index 2ea5d54b7de3e..8a0d8039bb7cd 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/restore_snapshot_form/steps/step_logistics.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/restore_snapshot_form/steps/step_logistics.tsx @@ -56,6 +56,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = label: index, checked: isAllIndices || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode typeof restoreIndices === 'string' || (Array.isArray(restoreIndices) && restoreIndices.includes(index)) ? 'on' @@ -97,7 +99,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =

@@ -113,7 +115,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = > @@ -234,6 +236,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = restoreIndices && restoreIndices.length > 0 ? ( { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed indicesOptions.forEach((option: Option) => { option.checked = undefined; }); @@ -252,6 +255,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = ) : ( { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed indicesOptions.forEach((option: Option) => { option.checked = 'on'; }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_error.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_error.tsx index 6d65addeb4cb9..2ad6f0870c140 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_error.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_error.tsx @@ -16,9 +16,15 @@ interface Props { message?: string; }; }; + actions?: JSX.Element; } -export const SectionError: React.FunctionComponent = ({ title, error, ...rest }) => { +export const SectionError: React.FunctionComponent = ({ + title, + error, + actions, + ...rest +}) => { const { error: errorString, cause, // wrapEsError() on the server adds a "cause" array @@ -27,10 +33,10 @@ export const SectionError: React.FunctionComponent = ({ title, error, ... return ( -
{message || errorString}
+ {cause ? message || errorString :

{message || errorString}

} {cause && ( - +
    {cause.map((causeMsg, i) => (
  • {causeMsg}
  • @@ -38,6 +44,7 @@ export const SectionError: React.FunctionComponent = ({ title, error, ...
)} + {actions ? actions : null}
); }; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_loading.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_loading.tsx index 4c6273682a0e4..aff3363c0aa60 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_loading.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/section_loading.tsx @@ -6,13 +6,37 @@ import React from 'react'; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, +} from '@elastic/eui'; interface Props { + inline?: boolean; children: React.ReactNode; + [key: string]: any; } -export const SectionLoading: React.FunctionComponent = ({ children }) => { +export const SectionLoading: React.FunctionComponent = ({ inline, children, ...rest }) => { + if (inline) { + return ( + + + + + + + {children} + + + + ); + } + return ( } diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts index 61722bada4d13..d95c243aeed62 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DAY } from '../../shared_imports'; + export const BASE_PATH = '/management/elasticsearch/snapshot_restore'; export const DEFAULT_SECTION: Section = 'snapshots'; export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policies'; @@ -86,6 +88,9 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST setting => !UNREMOVABLE_INDEX_SETTINGS.includes(setting) ); +export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?'; +export const DEFAULT_POLICY_FREQUENCY = DAY; + // UI Metric constants export const UIM_APP_NAME = 'snapshot_restore'; export const UIM_REPOSITORY_LIST_LOAD = 'repository_list_load'; @@ -112,3 +117,5 @@ export const UIM_POLICY_DETAIL_PANEL_HISTORY_TAB = 'policy_detail_panel_last_suc export const UIM_POLICY_EXECUTE = 'policy_execute'; export const UIM_POLICY_DELETE = 'policy_delete'; export const UIM_POLICY_DELETE_MANY = 'policy_delete_many'; +export const UIM_POLICY_CREATE = 'policy_create'; +export const UIM_POLICY_UPDATE = 'policy_update'; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/index.scss b/x-pack/legacy/plugins/snapshot_restore/public/app/index.scss index 8e42fb4598799..b680f4d3ebf90 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/index.scss +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/index.scss @@ -11,4 +11,5 @@ // snapshotRestore__legend-isLoading @import 'components/restore_snapshot_form/restore_snapshot_form'; +@import 'components/policy_form/policy_form'; @import 'sections/home/home'; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss index 96a08e40a3411..c714222daa98b 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss @@ -18,4 +18,14 @@ background: $euiColorLightestShade; } } +} + +/* + * 1. Make in progress snapshot loading indicator be centered vertically + * when it is inside tooltip wrapper + */ +.snapshotRestore__policyTable { + .euiToolTipAnchor { + display: flex; + } } \ No newline at end of file diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx index 2e1029204dbac..e3ec7675068ed 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx @@ -22,7 +22,7 @@ import { import { BASE_PATH, Section } from '../../constants'; import { useAppDependencies } from '../../index'; -import { breadcrumbService } from '../../services/navigation'; +import { breadcrumbService, docTitleService } from '../../services/navigation'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; @@ -92,10 +92,11 @@ export const SnapshotRestoreHome: React.FunctionComponent { - breadcrumbService.setBreadcrumbs('home'); - }, []); + breadcrumbService.setBreadcrumbs(section || 'home'); + docTitleService.setTitle(section || 'home'); + }, [section]); return ( diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx index ab658373283c8..e4b6ad28e324d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx @@ -16,6 +16,10 @@ import { EuiTabs, EuiTab, EuiButton, + EuiPopover, + EuiContextMenu, + EuiButtonIcon, + EuiLink, } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; @@ -26,6 +30,7 @@ import { } from '../../../../constants'; import { useLoadPolicy } from '../../../../services/http'; import { uiMetricService } from '../../../../services/ui_metric'; +import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigation'; import { SectionError, @@ -64,6 +69,7 @@ export const PolicyDetails: React.FunctionComponent = ({ const { trackUiMetric } = uiMetricService; const { error, data: policyDetails, sendRequest: reload } = useLoadPolicy(policyName); const [activeTab, setActiveTab] = useState(TAB_SUMMARY); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); // Reset tab when we look at a different policy useEffect(() => { @@ -183,53 +189,103 @@ export const PolicyDetails: React.FunctionComponent = ({ /> - {policyDetails ? ( - - - - {deletePolicyPrompt => { - return ( - deletePolicyPrompt([policyName], onPolicyDeleted)} - > - - - ); - }} - - - - - {executePolicyPrompt => { - return ( - - executePolicyPrompt(policyName, () => { - onPolicyExecuted(); - reload(); - }) - } - fill - color="primary" - data-test-subj="srPolicyDetailsExecuteActionButton" - > - - - ); - }} - - - + + {executePolicyPrompt => { + return ( + + {deletePolicyPrompt => { + return ( + setIsPopoverOpen(!isPopoverOpen)} + iconType="arrowDown" + fill + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + { + executePolicyPrompt(policyName, () => + // Wait a little bit for policy to execute before reloading policy table + // and policy details so that History tab information is updated with + // results of the execution + setTimeout(() => { + onPolicyExecuted(); + reload(); + }, 2000) + ); + }, + disabled: Boolean(policyDetails.policy.inProgress), + }, + { + name: i18n.translate( + 'xpack.snapshotRestore.policyDetails.editButtonLabel', + { + defaultMessage: 'Edit', + } + ), + icon: 'pencil', + href: linkToEditPolicy(policyName), + }, + { + name: i18n.translate( + 'xpack.snapshotRestore.policyDetails.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + icon: 'trash', + onClick: () => + deletePolicyPrompt([policyName], onPolicyDeleted), + }, + ], + }, + ]} + /> + + ); + }} + + ); + }} + ) : null} @@ -245,11 +301,49 @@ export const PolicyDetails: React.FunctionComponent = ({ maxWidth={550} > - -

- {policyName} -

-
+ + + + + +

+ {policyName} +

+
+ + reload()} + /> + +
+
+
+ {policyDetails && policyDetails.policy && policyDetails.policy.inProgress ? ( + + + + + + + + ) : null} +
{renderTabs()}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_history.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_history.tsx index 481a24b50b15d..0a8774c0c85a6 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_history.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_history.tsx @@ -55,12 +55,11 @@ export const TabHistory: React.FunctionComponent = ({ policy }) => { - + @@ -107,12 +106,11 @@ export const TabHistory: React.FunctionComponent = ({ policy }) => { - + @@ -140,7 +138,7 @@ export const TabHistory: React.FunctionComponent = ({ policy }) => { @@ -154,13 +152,13 @@ export const TabHistory: React.FunctionComponent = ({ policy }) => { setOptions={{ showLineNumbers: false, tabSize: 2, - maxLines: Infinity, }} editorProps={{ $blockScrolling: Infinity, }} minLines={6} - maxLines={6} + maxLines={12} + wrapEnabled={true} showGutter={false} aria-label={ = ({ policy }) => {

, time: , diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx index eadd9a9867b0b..ea29d6492cb4b 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx @@ -3,7 +3,7 @@ * 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, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -41,36 +41,45 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { nextExecutionMillis, config, } = policy; - const { includeGlobalState, ignoreUnavailable, indices, partial } = config; + const { includeGlobalState, ignoreUnavailable, indices, partial } = config || { + includeGlobalState: undefined, + ignoreUnavailable: undefined, + indices: undefined, + partial: undefined, + }; // Only show 10 indices initially const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState(false); - const hiddenIndicesCount = indices && indices.length > 10 ? indices.length - 10 : 0; + const displayIndices = typeof indices === 'string' ? indices.split(',') : indices; + const hiddenIndicesCount = + displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0; const shortIndicesList = - indices && indices.length ? ( -

    - {[...indices].splice(0, 10).map((index: string) => ( -
  • - - {index} - -
  • - ))} - {hiddenIndicesCount ? ( -
  • - - setIsShowingFullIndicesList(true)}> - {' '} - - - -
  • - ) : null} -
+ displayIndices && displayIndices.length ? ( + +
    + {[...displayIndices].splice(0, 10).map((index: string) => ( +
  • + + {index} + +
  • + ))} + {hiddenIndicesCount ? ( +
  • + + setIsShowingFullIndicesList(true)}> + {' '} + + + +
  • + ) : null} +
+
) : ( = ({ policy }) => { /> ); const fullIndicesList = - indices && indices.length && indices.length > 10 ? ( -
    - {indices.map((index: string) => ( -
  • - - {index} - -
  • - ))} - {hiddenIndicesCount ? ( -
  • - - setIsShowingFullIndicesList(false)}> - {' '} - - - -
  • - ) : null} -
+ displayIndices && displayIndices.length && displayIndices.length > 10 ? ( + +
    + {displayIndices.map((index: string) => ( +
  • + + {index} + +
  • + ))} + {hiddenIndicesCount ? ( +
  • + + setIsShowingFullIndicesList(false)}> + {' '} + + + +
  • + ) : null} +
+
) : null; + // Reset indices list state when clicking through different policies + useEffect(() => { + return () => { + setIsShowingFullIndicesList(false); + }; + }, []); + return ( @@ -180,7 +198,7 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { @@ -200,7 +218,7 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { - {isShowingFullIndicesList ? fullIndicesList : shortIndicesList} + {isShowingFullIndicesList ? fullIndicesList : shortIndicesList}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx index d94f3b0310387..a4664ea414526 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx @@ -7,13 +7,16 @@ import React, { Fragment, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { SlmPolicy } from '../../../../../common/types'; +import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; import { SectionError, SectionLoading } from '../../../components'; import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; import { useAppDependencies } from '../../../index'; import { useLoadPolicies } from '../../../services/http'; import { uiMetricService } from '../../../services/ui_metric'; +import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; +import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization'; import { PolicyDetails } from './policy_details'; import { PolicyTable } from './policy_table'; @@ -44,9 +47,7 @@ export const PolicyList: React.FunctionComponent { - return history.createHref({ - pathname: `${BASE_PATH}/policies/${newPolicyName}`, - }); + return linkToPolicy(newPolicyName); }; const closePolicyDetails = () => { @@ -72,7 +73,7 @@ export const PolicyList: React.FunctionComponent

} + 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 ? (
{content}
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": "索引",