From fbc60fb519628398ec40e1ff99ebe26dc3c11a20 Mon Sep 17 00:00:00 2001 From: iso9000t <119494473+iso9000t@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:33:18 +0400 Subject: [PATCH] EPMRPP-98425 || Duplicate dashboard on current project --- app/src/common/urls.js | 7 +- .../controllers/dashboard/actionCreators.js | 12 +++ app/src/controllers/dashboard/constants.js | 2 + app/src/controllers/dashboard/index.js | 2 + app/src/controllers/dashboard/reducer.js | 3 + app/src/controllers/dashboard/sagas.js | 38 +++++++ .../dashboardTable/dashboardTable.jsx | 20 +++- .../dashboardTable/dashboardTable.scss | 102 ++++++++++++++++-- .../dashboardTable/dashboardTableColumns.jsx | 76 ++++++++++++- .../inside/dashboardPage/dashboardPage.jsx | 23 ++++ .../modals/addEditModal/addEditModal.jsx | 73 ++++++++++--- 11 files changed, 331 insertions(+), 27 deletions(-) diff --git a/app/src/common/urls.js b/app/src/common/urls.js index 40eb9817ea..3a14721b24 100644 --- a/app/src/common/urls.js +++ b/app/src/common/urls.js @@ -41,6 +41,8 @@ export const URLS = { dashboard: (activeProject, id) => `${urlBase}${activeProject}/dashboard/${id}`, dashboards: (activeProject, params) => `${urlBase}${activeProject}/dashboard${getQueryParams({ ...params })}`, + dashboardConfig: (activeProject, id) => `${urlBase}${activeProject}/dashboard/${id}/config`, + dashboardPreconfigured: (activeProject) => `${urlBase}${activeProject}/dashboard/preconfigured`, widget: (activeProject, widgetId = '') => `${urlBase}${activeProject}/widget/${widgetId}`, widgetMultilevel: (activeProject, widgetId, params) => @@ -306,7 +308,10 @@ export const URLS = { btsIntegrationPostTicket: (projectId, integrationId) => `${urlBase}bts/${projectId}/${integrationId}/ticket`, btsTicket: (activeProject, issueId, btsProject, btsUrl) => - `${urlBase}bts/${activeProject}/ticket/${issueId}${getQueryParams({ btsProject, btsUrl })}`, + `${urlBase}bts/${activeProject}/ticket/${issueId}${getQueryParams({ + btsProject, + btsUrl, + })}`, runUniqueErrorAnalysis: (activeProject) => `${urlBase}${activeProject}/launch/cluster`, clusterByLaunchId: (activeProject, launchId, query) => `${urlBase}${activeProject}/launch/cluster/${launchId}${getQueryParams(query)}`, diff --git a/app/src/controllers/dashboard/actionCreators.js b/app/src/controllers/dashboard/actionCreators.js index be5a1111ca..cb9b6a52ac 100644 --- a/app/src/controllers/dashboard/actionCreators.js +++ b/app/src/controllers/dashboard/actionCreators.js @@ -30,6 +30,8 @@ import { UPDATE_DASHBOARD, UPDATE_DASHBOARD_SUCCESS, UPDATE_DASHBOARD_WIDGETS, + DUPLICATE_DASHBOARD, + DUPLICATE_DASHBOARD_SUCCESS, } from './constants'; export const fetchDashboardsAction = (params) => ({ @@ -51,6 +53,16 @@ export const addDashboardSuccessAction = (item) => ({ payload: item, }); +export const duplicateDashboardAction = (item) => ({ + type: DUPLICATE_DASHBOARD, + payload: item, +}); + +export const duplicateDashboardSuccessAction = (item) => ({ + type: DUPLICATE_DASHBOARD_SUCCESS, + payload: item, +}); + export const updateDashboardAction = (item) => ({ type: UPDATE_DASHBOARD, payload: item, diff --git a/app/src/controllers/dashboard/constants.js b/app/src/controllers/dashboard/constants.js index 1a95b9f146..e315e10a86 100644 --- a/app/src/controllers/dashboard/constants.js +++ b/app/src/controllers/dashboard/constants.js @@ -36,6 +36,8 @@ export const ADD_DASHBOARD_SUCCESS = 'addDashboardSuccess'; export const UPDATE_DASHBOARD = 'updateDashboard'; export const UPDATE_DASHBOARD_WIDGETS = 'updateDashboardWidgets'; export const UPDATE_DASHBOARD_SUCCESS = 'updateDashboardSuccess'; +export const DUPLICATE_DASHBOARD = 'duplicateDashboard'; +export const DUPLICATE_DASHBOARD_SUCCESS = 'duplicateDashboardSuccess'; export const REMOVE_DASHBOARD = 'removeDashboard'; export const REMOVE_DASHBOARD_SUCCESS = 'removeDashboardSuccess'; export const CHANGE_VISIBILITY_TYPE = 'changeVisibilityType'; diff --git a/app/src/controllers/dashboard/index.js b/app/src/controllers/dashboard/index.js index 110954062a..3a8f173f70 100644 --- a/app/src/controllers/dashboard/index.js +++ b/app/src/controllers/dashboard/index.js @@ -24,6 +24,8 @@ export { updateDashboardWidgetsAction, toggleFullScreenModeAction, changeFullScreenModeAction, + duplicateDashboardAction, + duplicateDashboardSuccessAction, } from './actionCreators'; export { dashboardReducer } from './reducer'; export { diff --git a/app/src/controllers/dashboard/reducer.js b/app/src/controllers/dashboard/reducer.js index ec06894c04..ec3a31e429 100644 --- a/app/src/controllers/dashboard/reducer.js +++ b/app/src/controllers/dashboard/reducer.js @@ -36,6 +36,7 @@ import { REMOVE_DASHBOARD_SUCCESS, TOGGLE_FULL_SCREEN_MODE, UPDATE_DASHBOARD_SUCCESS, + DUPLICATE_DASHBOARD_SUCCESS, } from './constants'; const dashboardsReducer = (state = INITIAL_STATE.dashboards, { type = '', payload = {} }) => { @@ -46,6 +47,8 @@ const dashboardsReducer = (state = INITIAL_STATE.dashboards, { type = '', payloa return state.map((item) => (item.id === payload.id ? payload : item)); case REMOVE_DASHBOARD_SUCCESS: return state.filter((item) => item.id !== payload); + case DUPLICATE_DASHBOARD_SUCCESS: + return [...state, payload]; default: return state; } diff --git a/app/src/controllers/dashboard/sagas.js b/app/src/controllers/dashboard/sagas.js index b589d4bdc9..998636c864 100644 --- a/app/src/controllers/dashboard/sagas.js +++ b/app/src/controllers/dashboard/sagas.js @@ -47,6 +47,7 @@ import { REMOVE_DASHBOARD_SUCCESS, INCREASE_TOTAL_DASHBOARDS_LOCALLY, DECREASE_TOTAL_DASHBOARDS_LOCALLY, + DUPLICATE_DASHBOARD, } from './constants'; import { dashboardItemsSelector, querySelector } from './selectors'; import { @@ -131,6 +132,42 @@ function* addDashboard({ payload: dashboard }) { }); } +function* duplicateDashboard({ payload: dashboard }) { + const activeProject = yield select(activeProjectSelector); + try { + const config = yield call(fetch, URLS.dashboardConfig(activeProject, dashboard.id)); + + const result = yield call(fetch, URLS.dashboardPreconfigured(activeProject), { + method: 'post', + data: { + name: dashboard.name, + description: dashboard.description, + config, + }, + }); + + yield put( + addDashboardSuccessAction({ + id: result.id, + name: dashboard.name, + description: dashboard.description, + owner: dashboard.owner, + widgets: [], // or config.widgets if available + }), + ); + + yield put( + showNotification({ + message: 'Dashboard successfully duplicated', + type: NOTIFICATION_TYPES.SUCCESS, + }), + ); + yield put(hideModalAction()); + } catch (error) { + yield put(showDefaultErrorNotification(error)); + } +} + function* updateDashboard({ payload: dashboard }) { const activeProject = yield select(activeProjectSelector); const { name, description, id } = dashboard; @@ -205,5 +242,6 @@ export function* dashboardSagas() { yield takeEvery(REMOVE_DASHBOARD, removeDashboard), yield takeEvery(CHANGE_VISIBILITY_TYPE, changeVisibilityType), yield takeEvery(REMOVE_DASHBOARD_SUCCESS, redirectAfterDelete), + yield takeEvery(DUPLICATE_DASHBOARD, duplicateDashboard), ]); } diff --git a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.jsx b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.jsx index 5e104cf434..3042c52a69 100644 --- a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.jsx +++ b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.jsx @@ -28,6 +28,7 @@ import { OwnerColumn, EditColumn, DeleteColumn, + DuplicateColumn, } from './dashboardTableColumns'; import styles from './dashboardTable.scss'; @@ -45,6 +46,10 @@ const messages = defineMessages({ id: 'DashboardTable.owner', defaultMessage: 'Owner', }, + duplicate: { + id: 'DashboardTable.duplicate', + defaultMessage: 'Duplicate', + }, edit: { id: 'DashboardTable.edit', defaultMessage: 'Edit', @@ -64,6 +69,7 @@ export class DashboardTable extends Component { intl: PropTypes.object.isRequired, onDeleteItem: PropTypes.func, onEditItem: PropTypes.func, + onDuplicate: PropTypes.func, onAddItem: PropTypes.func, projectId: PropTypes.string, dashboardItems: PropTypes.array, @@ -74,6 +80,7 @@ export class DashboardTable extends Component { static defaultProps = { onDeleteItem: () => {}, onEditItem: () => {}, + onDuplicate: () => {}, onAddItem: () => {}, projectId: '', dashboardItems: [], @@ -82,7 +89,7 @@ export class DashboardTable extends Component { }; getTableColumns() { - const { onDeleteItem, onEditItem, intl, projectId } = this.props; + const { onDeleteItem, onEditItem, onDuplicate, intl, projectId } = this.props; return [ { @@ -111,6 +118,17 @@ export class DashboardTable extends Component { formatter: (value) => value.owner, component: OwnerColumn, }, + { + title: { + full: intl.formatMessage(messages.duplicate), + short: intl.formatMessage(messages.duplicate), + }, + component: DuplicateColumn, + customProps: { + onDuplicate, + }, + align: ALIGN_CENTER, + }, { title: { full: intl.formatMessage(messages.edit), diff --git a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.scss b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.scss index c2a541e013..6dc356a6da 100644 --- a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.scss +++ b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTable.scss @@ -57,8 +57,21 @@ } .icon-holder { - border-left: 1px solid $COLOR--gray-91; min-height: 20px; + display: flex; + align-items: center; + justify-content: center; + + &.no-border { + border-left: none; + } +} + +.edit-cell, +.delete-cell { + .icon-holder { + border-left: 1px solid $COLOR--gray-91; + } } .dashboard-table { @@ -66,6 +79,87 @@ padding-bottom: 15px; } +.with-button { + i { + cursor: pointer; + } +} + +.duplicate-dropdown { + position: relative; + display: flex; + align-items: center; + padding-right: 15px; + cursor: pointer; + color: $COLOR--gray-60; + + &:hover { + color: $COLOR--gray-47; + } +} + +.duplicate-icon { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 20px; + + svg { + height: 30px; + width: 30px; + stroke-width: 1.5; + } +} + +.arrow { + position: absolute; + right: 2px; + top: 50%; + margin-top: -1px; + width: 0; + height: 0; + border-top: 4px solid currentColor; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + box-sizing: border-box; + transition: transform 200ms linear; + + &.opened { + transform: rotate(180deg); + } +} + +.hamburger-menu { + position: absolute; + top: 100%; + right: -25px; + z-index: $Z-INDEX-POPUP; + margin-top: 4px; + background: $COLOR--white-two; + border: 1px solid $COLOR--gray-91; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + border-radius: 4px; + min-width: max-content; + white-space: nowrap; + display: none; + + &.shown { + display: block; + } +} + +.dropdown-item { + padding: 8px 15px; + font-size: 13px; + color: $COLOR--charcoal-grey; + text-align: left; + + &:hover { + background: $COLOR--tealish-hover; + } +} + @media (max-width: $SCREEN_SM_MAX) { .name, .description, @@ -82,9 +176,3 @@ } } } - -.with-button { - i { - cursor: pointer; - } -} diff --git a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTableColumns.jsx b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTableColumns.jsx index 28f4cf9fe1..12aa94b670 100644 --- a/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTableColumns.jsx +++ b/app/src/pages/inside/dashboardPage/dashboardList/dashboardTable/dashboardTableColumns.jsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import classNames from 'classnames/bind'; import PropTypes from 'prop-types'; import track from 'react-tracking'; @@ -22,6 +22,8 @@ import { Icon } from 'components/main/icon'; import { PROJECT_DASHBOARD_ITEM_PAGE } from 'controllers/pages'; import { NavLink } from 'components/main/navLink'; import { DASHBOARD_EVENTS } from 'analyticsEvents/dashboardsPageEvents'; +import Parser from 'html-react-parser'; +import IconDuplicate from 'common/img/duplicate-inline.svg'; import styles from './dashboardTable.scss'; const cx = classNames.bind(styles); @@ -77,6 +79,70 @@ OwnerColumn.defaultProps = { className: '', }; +export const DuplicateColumn = track()( + ({ value, customProps, className, tracking: { trackEvent } }) => { + const [opened, setOpened] = useState(false); + + useEffect(() => { + const handleOutsideClick = (e) => { + if (!e.target.closest(`.${cx('duplicate-dropdown')}`) && opened) { + setOpened(false); + } + }; + + document.addEventListener('click', handleOutsideClick); + return () => document.removeEventListener('click', handleOutsideClick); + }, [opened]); + + const handleDuplicate = () => { + const { id } = value; + trackEvent(DASHBOARD_EVENTS.clickOnIconDashboard('duplicate', id)); + customProps.onDuplicate(value); + setOpened(false); + }; + + const handleCopyConfig = () => { + // TODO: Copy configuration functionality will be added later + setOpened(false); + }; + + return ( +