Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import SignupSuccess from '../routes/signup/5-success';
import Dashboard from '../routes/dashboard';
import NewDashboard from '../routes/dashboard/new-dashboard';
import EditDashboard from '../routes/dashboard/edit-dashboard';
import DuplicateDashboard from '../routes/dashboard/duplicate-dashboard';

import IntegrationPage from '../routes/integration';
import DevicesListPage from '../routes/devices';
Expand Down Expand Up @@ -266,6 +267,7 @@ const AppRouter = connect(
<SafeAsyncRoute path="/dashboard" component={Dashboard} />
<SafeAsyncRoute path="/dashboard/:dashboardSelector" component={Dashboard} />
<EditDashboard path="/dashboard/:dashboardSelector/edit" />
<DuplicateDashboard path="/dashboard/:dashboardSelector/duplicate" />
<NewDashboard path="/dashboard/create/new" />
<SafeAsyncRoute path="/dashboard/integration" component={IntegrationPage} />

Expand Down
12 changes: 12 additions & 0 deletions front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
"disableFullScreen": "Vollbild beenden",
"editDashboardTitle": "Dashboard bearbeiten",
"editDashboardDeleteButton": "Löschen",
"editDashboardDuplicateButton": "Duplizieren",
"editDashboardCancelButton": "Abbrechen",
"editDashboardDeleteText": "Bist du sicher, dass du dieses Dashboard löschen möchtest?",
"toggleDefineTabletMode": "Tablet-Modus",
Expand Down Expand Up @@ -4007,6 +4008,17 @@
"invalidIcon": "Symbol ist erforderlich",
"sceneAlreadyExist": "Eine Szene mit diesem Namen existiert bereits."
},
"duplicateDashboard": {
"cardTitle": "Dashboard \"{{name}}\" duplizieren",
"nameLabel": "Name",
"namePlaceholder": "Gib einen neuen Namen ein",
"duplicateDashboardButton": "Duplizieren",
"invalidName": "Name ist erforderlich",
"dashboardAlreadyExist": "Ein Dashboard mit diesem Namen existiert bereits.",
"unknownError": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut!",
"nameAfterCopy": "Kopie von {{name}}",
"savedVersionInfo": "Die Kopie wird von der zuletzt gespeicherten Version dieses Dashboards erstellt. Nicht gespeicherte Änderungen werden nicht kopiert."
},
"duplicateScene": {
"cardTitle": "Szene \"{{name}}\" duplizieren",
"nameLabel": "Name",
Expand Down
12 changes: 12 additions & 0 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
"disableFullScreen": "Exit full screen",
"editDashboardTitle": "Edit dashboard",
"editDashboardDeleteButton": "Delete",
"editDashboardDuplicateButton": "Duplicate",
"editDashboardCancelButton": "Cancel",
"editDashboardDeleteText": "Are you sure you want to delete this dashboard?",
"toggleDefineTabletMode": "Tablet Mode",
Expand Down Expand Up @@ -4007,6 +4008,17 @@
"invalidIcon": "Icon is required",
"sceneAlreadyExist": "A scene with that name already exists."
},
"duplicateDashboard": {
"cardTitle": "Duplicate dashboard \"{{name}}\"",
"nameLabel": "Name",
"namePlaceholder": "Enter a new name",
"duplicateDashboardButton": "Duplicate",
"invalidName": "Name is required",
"dashboardAlreadyExist": "A dashboard with this name already exists.",
"unknownError": "An unknown error occurred. Please retry!",
"nameAfterCopy": "Copy of {{name}}",
"savedVersionInfo": "The copy is made from the last saved version of this dashboard. Any change not saved yet will not be copied."
},
"duplicateScene": {
"cardTitle": "Duplicate scene \"{{name}}\"",
"nameLabel": "Name",
Expand Down
12 changes: 12 additions & 0 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
"newDashboardButton": "Nouveau",
"editDashboardCancelButton": "Annuler",
"editDashboardDeleteButton": "Supprimer",
"editDashboardDuplicateButton": "Dupliquer",
"editDashboardSaveButton": "Sauvegarder",
"editDashboardDeleteText": "Êtes-vous sûr de vouloir supprimer ce tableau de bord ?",
"toggleDefineTabletMode": "Mode tablette",
Expand Down Expand Up @@ -4007,6 +4008,17 @@
"invalidIcon": "L'icône est requise",
"sceneAlreadyExist": "Une scène avec le même nom existe déjà."
},
"duplicateDashboard": {
"cardTitle": "Dupliquer le tableau de bord \"{{name}}\"",
"nameLabel": "Nom",
"namePlaceholder": "Entrez un nouveau nom",
"duplicateDashboardButton": "Dupliquer",
"invalidName": "Le nom est requis",
"dashboardAlreadyExist": "Un tableau de bord avec le même nom existe déjà.",
"unknownError": "Une erreur inconnue est survenue. Veuillez réessayer !",
"nameAfterCopy": "Copie de {{name}}",
"savedVersionInfo": "La copie est réalisée à partir de la dernière version enregistrée de ce tableau de bord. Les modifications non enregistrées ne seront pas copiées."
},
"duplicateScene": {
"cardTitle": "Dupliquer la scène \"{{name}}\"",
"nameLabel": "Nom",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Text, Localizer } from 'preact-i18n';
import cx from 'classnames';
import get from 'get-value';

import { RequestStatus } from '../../../utils/consts';
import style from './style.css';

const DuplicateDashboardPage = ({ children, ...props }) => (
<div class={cx('container', style.containerWithMargin)}>
<button onClick={props.goBack} class="btn btn-secondary btn-sm">
<Text id="global.backButton" />
</button>

<div class="row">
<div class="col col-login mx-auto">
<form onSubmit={props.duplicateDashboard} class="card">
<div class={props.loading ? 'dimmer active' : 'dimmer'}>
<div class="loader" />
<div class="dimmer-content">
<div class="card-body p-6">
<div class="card-title">
<Text id="duplicateDashboard.cardTitle" fields={{ name: props.sourceDashboard.name }} />
</div>
<div class="alert alert-info">
<Text id="duplicateDashboard.savedVersionInfo" />
</div>
{props.duplicateDashboardStatus === RequestStatus.ConflictError && (
<div class="alert alert-danger">
<Text id="duplicateDashboard.dashboardAlreadyExist" />
</div>
)}
{props.duplicateDashboardStatus === RequestStatus.Error && (
<div class="alert alert-danger">
<Text id="duplicateDashboard.unknownError" />
</div>
)}
<div class="form-group">
<label class="form-label">
<Text id="duplicateDashboard.nameLabel" />
</label>
<Localizer>
<input
type="text"
class={cx('form-control', {
'is-invalid': get(props, 'duplicateDashboardErrors.name')
})}
disabled={props.loading}
placeholder={<Text id="duplicateDashboard.namePlaceholder" />}
value={get(props, 'dashboard.name')}
onInput={props.updateDuplicateDashboardName}
/>
</Localizer>
<div class="invalid-feedback">
<Text id="duplicateDashboard.invalidName" />
</div>
</div>
<div class="form-footer">
<button
onClick={props.duplicateDashboard}
class="btn btn-primary btn-block"
disabled={props.duplicateDashboardStatus === RequestStatus.Getting}
>
<Text id="duplicateDashboard.duplicateDashboardButton" />
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
);

export default DuplicateDashboardPage;
130 changes: 130 additions & 0 deletions front/src/routes/dashboard/duplicate-dashboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { route } from 'preact-router';
import get from 'get-value';

import DuplicateDashboardPage from './DuplicateDashboardPage';
import { RequestStatus } from '../../../utils/consts';
import withIntlAsProp from '../../../utils/withIntlAsProp';

class DuplicateDashboard extends Component {
goBack = () => {
route(`/dashboard/${this.props.dashboardSelector}/edit`);
};

getSourceDashboard = async () => {
try {
const sourceDashboard = await this.props.httpClient.get(`/api/v1/dashboard/${this.props.dashboardSelector}`);
this.setState({
sourceDashboard,
loading: false,
dashboard: {
name: get(this.props.intl.dictionary, 'duplicateDashboard.nameAfterCopy').replace(
'{{name}}',
sourceDashboard.name
)
}
});
} catch (e) {
console.error(e);
this.setState({
loading: false,
duplicateDashboardStatus: RequestStatus.Error
});
}
};

checkErrors = (name = this.state.dashboard.name) => {
const duplicateDashboardErrors = {};
if (!name) {
duplicateDashboardErrors.name = true;
}
this.setState({
duplicateDashboardErrors
});
return Object.keys(duplicateDashboardErrors).length > 0;
};

updateDuplicateDashboardName = e => {
const { value } = e.target;
this.setState({
dashboard: {
name: value
}
});
if (this.state.duplicateDashboardErrors) {
// setState is asynchronous, so we validate the new value directly
this.checkErrors(value);
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

duplicateDashboard = async e => {
e.preventDefault();
// if errored, we don't continue
if (this.checkErrors()) {
return;
}
this.setState({
duplicateDashboardStatus: RequestStatus.Getting
});
try {
const duplicatedDashboard = await this.props.httpClient.post(
`/api/v1/dashboard/${this.props.dashboardSelector}/duplicate`,
this.state.dashboard
);
this.setState({
duplicateDashboardStatus: RequestStatus.Success
});
route(`/dashboard/${duplicatedDashboard.selector}/edit`);
} catch (e) {
console.error(e);
const status = get(e, 'response.status');
if (status === 409) {
this.setState({
duplicateDashboardStatus: RequestStatus.ConflictError
});
} else {
this.setState({
duplicateDashboardStatus: RequestStatus.Error
});
}
}
};

constructor(props) {
super(props);
this.state = {
dashboard: {
name: ''
},
sourceDashboard: {
name: ''
},
loading: true,
duplicateDashboardErrors: null,
duplicateDashboardStatus: null
};
}

componentDidMount() {
this.getSourceDashboard();
}

render(props, { dashboard, sourceDashboard, loading, duplicateDashboardErrors, duplicateDashboardStatus }) {
return (
<DuplicateDashboardPage
{...props}
goBack={this.goBack}
dashboard={dashboard}
sourceDashboard={sourceDashboard}
loading={loading}
updateDuplicateDashboardName={this.updateDuplicateDashboardName}
duplicateDashboard={this.duplicateDashboard}
duplicateDashboardErrors={duplicateDashboardErrors}
duplicateDashboardStatus={duplicateDashboardStatus}
/>
);
}
}

export default withIntlAsProp(connect('httpClient', {})(DuplicateDashboard));
3 changes: 3 additions & 0 deletions front/src/routes/dashboard/duplicate-dashboard/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.containerWithMargin {
margin-top: 3rem;
}
3 changes: 3 additions & 0 deletions front/src/routes/dashboard/edit-dashboard/EditActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const EditActions = props => (
<button onClick={props.askDeleteCurrentDashboard} className="btn btn-outline-danger btn-sm ml-2">
<Text id="dashboard.editDashboardDeleteButton" /> <i class="fe fe-trash" />
</button>
<button onClick={props.duplicateCurrentDashboard} className="btn btn-outline-secondary btn-sm ml-2">
<Text id="dashboard.editDashboardDuplicateButton" /> <i class="fe fe-copy" />
</button>
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
<button onClick={props.saveDashboard} className="btn btn-outline-primary btn-sm ml-2">
<Text id="dashboard.editDashboardSaveButton" /> <i class="fe fe-check" />
</button>
Expand Down
5 changes: 5 additions & 0 deletions front/src/routes/dashboard/edit-dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ class EditDashboard extends Component {
}
};

duplicateCurrentDashboard = () => {
route(`/dashboard/${this.state.currentDashboard.selector}/duplicate`);
Comment thread
cursor[bot] marked this conversation as resolved.
};

askDeleteCurrentDashboard = async () => {
await this.setState({
askDeleteDashboard: true
Expand Down Expand Up @@ -426,6 +430,7 @@ class EditDashboard extends Component {
updateBoxConfig={this.updateBoxConfig}
updateCurrentDashboardName={this.updateCurrentDashboardName}
updateCurrentDashboardVisibility={this.updateCurrentDashboardVisibility}
duplicateCurrentDashboard={this.duplicateCurrentDashboard}
askDeleteCurrentDashboard={this.askDeleteCurrentDashboard}
cancelDeleteCurrentDashboard={this.cancelDeleteCurrentDashboard}
deleteCurrentDashboard={this.deleteCurrentDashboard}
Expand Down
13 changes: 13 additions & 0 deletions server/api/controllers/dashboard.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ module.exports = function DashboardController(gladys) {
res.json(dashboard);
}

/**
* @api {post} /api/v1/dashboard/:dashboard_selector/duplicate duplicate
* @apiName duplicate
* @apiGroup Dashboard
* @apiParam {String} name Name of the duplicated dashboard.
* @apiUse DashboardSuccess
*/
async function duplicate(req, res) {
const dashboard = await gladys.dashboard.duplicate(req.user.id, req.params.dashboard_selector, req.body.name);
res.status(201).json(dashboard);
}

/**
* @api {post} /api/v1/dashboard/order updateOrder
* @apiName updateOrder
Expand Down Expand Up @@ -101,6 +113,7 @@ module.exports = function DashboardController(gladys) {
return Object.freeze({
create: asyncMiddleware(create),
destroy: asyncMiddleware(destroy),
duplicate: asyncMiddleware(duplicate),
get: asyncMiddleware(get),
getBySelector: asyncMiddleware(getBySelector),
update: asyncMiddleware(update),
Expand Down
4 changes: 4 additions & 0 deletions server/api/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ function getRoutes(gladys) {
authenticated: true,
controller: dashboardController.destroy,
},
'post /api/v1/dashboard/:dashboard_selector/duplicate': {
authenticated: true,
controller: dashboardController.duplicate,
},
// device
'post /api/v1/device': {
authenticated: true,
Expand Down
Loading
Loading