Skip to content
Open
Show file tree
Hide file tree
Changes from all 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>
<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