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
3 changes: 2 additions & 1 deletion build/plotcss.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ var rules = {
"X .plotly-cloud-dialog .plotly-cloud-dialog-box": "box-sizing:border-box;min-width:300px;max-width:420px;padding:20px 24px;background-color:#fff;border:1px solid #e0e2e5;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,.25);font-size:13px;color:#2a3f5f;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-title": "font-size:16px;font-weight:bold;margin-bottom:12px;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-message": "line-height:1.5;overflow-wrap:break-word;word-wrap:break-word;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-message--hostname": "font-weight:bold;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-message--hostname": "font-weight:bold;text-decoration:underline;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-message--account": "margin-top:16px;padding:8px;border-radius:3px;font-size:.9em;background-color:#edf1f8;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-buttons": "display:flex;justify-content:flex-end;margin-top:20px;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-btn": "font-family:inherit;font-size:13px;padding:7px 16px;margin-left:8px;border-radius:3px;border:1px solid rgba(0,0,0,0);cursor:pointer;",
"X .plotly-cloud-dialog .plotly-cloud-dialog-btn:focus-visible": "outline:2px solid #447adb;outline-offset:1px;",
Expand Down
1 change: 1 addition & 0 deletions draftlogs/7928_change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Update "Share Chart" dialog with more informative wording [[#7928](https://github.com/plotly/plotly.js/pull/7928)]
16 changes: 10 additions & 6 deletions src/components/modebar/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,29 @@ modeBarButtons.toImage = {
modeBarButtons.sendChartToCloud = {
name: 'sendChartToCloud',
title: function (gd) {
return _(gd, 'Share Chart');
return _(gd, 'Share Chart...');
},
icon: Icons.cloudupload,
click: function (gd) {
var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL;
if (!baseUrl) {
console.error('No destination URL provided (plotlyServerURL is not set)');
console.error('No destination URL provided (plotlyServerURL is empty)');
return;
}

// Plotly Cloud origin, used to validate incoming messages and to target outgoing ones.
// `baseUrl` (plotlyServerURL) is the upload page that handles login and signals
// back when authentication succeeds.
// Validate that the provided plotlyServerURL is a valid URL
// with an http or https protocol
var baseUrlObj;
try {
new URL(baseUrl);
baseUrlObj = new URL(baseUrl);
} catch (e) {
console.error('Invalid plotlyServerURL: ' + baseUrl);
return;
}
if (baseUrlObj.protocol !== 'https:' && baseUrlObj.protocol !== 'http:') {
console.error('Invalid protocol for plotlyServerURL: ' + baseUrl);
return;
}

confirmCloudDialog(gd, baseUrl, function () {
Plots.sendDataToCloud(gd, baseUrl);
Expand Down
152 changes: 112 additions & 40 deletions src/components/modebar/cloud_confirm.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,99 @@
'use strict';

var d3 = require('@plotly/d3');
const d3 = require('@plotly/d3');

var _ = require('../../lib')._;
const _ = require('../../lib')._;
const dfltConfig = require('../../plot_api/plot_config').dfltConfig;

const buildDialogBox = (gd, overlay, serverUrl, onClickConfirm, onClickCancel) => {
// Wording for dialog box. Must be defined inside this function rather than
// at the top of the file because localization requires a reference to the
// graph div (gd)
const DIALOG_TITLE = _(gd, 'Share Chart');

// Messages to be shown when serverUrl matches the default (Plotly Cloud) URL
const DIALOG_MESSAGE_CLOUD = _(gd, 'This chart will be uploaded to {Plotly Cloud} to create a sharing link. Only you can see it until you change its visibility.');
const DIALOG_MESSAGE_CLOUD_ACCOUNT = _(gd, "If you don't have a Plotly Cloud account yet, you'll have a chance to create one.");

// Message to be shown when serverUrl is not the default URL
const DIALOG_MESSAGE_OTHER = _(gd, 'This chart will be sent to {serverUrl}.');

// Labels for buttons
const DIALOG_CANCEL = _(gd, 'Cancel');
const DIALOG_CONFIRM = _(gd, 'Share');

const dialog = overlay.append('div')
.classed('plotly-cloud-dialog-box', true);

dialog.append('div')
.classed('plotly-cloud-dialog-title', true)
.text(DIALOG_TITLE);

if (serverUrl === dfltConfig.plotlyServerURL) {
// If serverUrl matches the default Plotly Cloud URL,
// show a custom message designed for Plotly Cloud
const description = dialog.append('div')
.classed('plotly-cloud-dialog-message', true);

// Link to the base domain only, leaving the endpoint path
const serverUrlHref = new URL(serverUrl).origin;

// Split description into three parts: Before {, between, and after }
const descriptionParts = DIALOG_MESSAGE_CLOUD.split(/(\{|\})/);
const beforePart = descriptionParts[0];
const betweenPart = descriptionParts[2];
const afterPart = descriptionParts[4];

// Append the parts to the description div
description.append('span').text(beforePart);
description.append('a')
.classed('plotly-cloud-dialog-message--hostname', true)
.attr('href', serverUrlHref)
.attr('target', '_blank')
.text(betweenPart);
description.append('span').text(afterPart);

description.append('div')
.classed('plotly-cloud-dialog-message--account', true)
.text(DIALOG_MESSAGE_CLOUD_ACCOUNT);
} else {
// Otherwise, show a generic message with the server URL
// We can trust that serverUrl is a valid URL because it was validated in buttons.js
const serverUrlObj = new URL(serverUrl);
const serverUrlHostname = serverUrlObj.hostname;
// Link to the base domain only, leaving off any endpoint path
const serverUrlHref = serverUrlObj.origin;
const descriptionParts = DIALOG_MESSAGE_OTHER.split(/(\{|\})/);
const beforePart = descriptionParts[0];
const afterPart = descriptionParts[4];

const description = dialog.append('div')
.classed('plotly-cloud-dialog-message', true);

description.append('span').text(beforePart);
description.append('a')
.classed('plotly-cloud-dialog-message--hostname', true)
.attr('href', serverUrlHref)
.attr('target', '_blank')
.text(serverUrlHostname);
description.append('span').text(afterPart);
}

const buttons = dialog.append('div')
.classed('plotly-cloud-dialog-buttons', true);

buttons.append('button')
.classed('plotly-cloud-dialog-btn', true)
.classed('plotly-cloud-dialog-btn--cancel', true)
.text(DIALOG_CANCEL)
.on('click', onClickCancel);

buttons.append('button')
.classed('plotly-cloud-dialog-btn', true)
.classed('plotly-cloud-dialog-btn--confirm', true)
.text(DIALOG_CONFIRM)
.on('click', onClickConfirm);
};

/**
* Show a styled confirmation dialog before sharing a chart with Plotly Cloud.
Expand All @@ -15,61 +106,42 @@ var _ = require('../../lib')._;
* @param {string} serverUrl - destination shown in the dialog message
* @param {function} onConfirm - called when the user confirms the upload
*/
module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) {
var container = d3.select(gd._fullLayout._paperdiv.node());
const confirmCloudDialog = (gd, serverUrl, onConfirm) => {
const container = d3.select(gd._fullLayout._paperdiv.node());

// Never stack dialogs - drop any that is already open.
container.selectAll('.plotly-cloud-dialog').remove();

var overlay = container
const overlay = container
.append('div')
.classed('plotly-cloud-dialog', true);

var dialog = overlay.append('div')
.classed('plotly-cloud-dialog-box', true);

dialog.append('div')
.classed('plotly-cloud-dialog-title', true)
.text(_(gd, 'Share with Plotly Cloud'));

var serverUrlText = new URL(serverUrl).hostname;

var description = dialog.append('div');
description.classed('plotly-cloud-dialog-message', true);
description.append('span').text(_(gd, 'This chart and its data will be sent to '));
description.append('span').text(serverUrlText).classed('plotly-cloud-dialog-message--hostname', true);
description.append('span').text('. ');

var buttons = dialog.append('div')
.classed('plotly-cloud-dialog-buttons', true);

function close() {
const close = () => {
overlay.remove();
document.removeEventListener('keydown', onKeydown);
}
};

function onKeydown(e) {
const onKeydown = (e) => {
if(e.key === 'Escape' || e.keyCode === 27) close();
}
};
document.addEventListener('keydown', onKeydown);

// Clicking the backdrop (but not the dialog box) cancels.
overlay.on('click', function() {
overlay.on('click', () => {
if(d3.event.target === overlay.node()) close();
});

buttons.append('button')
.classed('plotly-cloud-dialog-btn', true)
.classed('plotly-cloud-dialog-btn--cancel', true)
.text(_(gd, 'Cancel'))
.on('click', close);

buttons.append('button')
.classed('plotly-cloud-dialog-btn', true)
.classed('plotly-cloud-dialog-btn--confirm', true)
.text(_(gd, 'Share'))
.on('click', function() {
// Build the dialog box and append it to the overlay
buildDialogBox(
gd,
overlay,
serverUrl,
() => {
close();
onConfirm();
});
},
close
);
};

module.exports = confirmCloudDialog;
9 changes: 9 additions & 0 deletions src/css/_cloud_dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@

&--hostname {
font-weight: bold;
text-decoration: underline;
}

&--account {
margin-top: 16px;
padding: 8px;
border-radius: 3px;
font-size: 0.9em;
background-color: vars.$color-bg-hint;
}
}

Expand Down
87 changes: 85 additions & 2 deletions test/jasmine/tests/config_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var Plotly = require('../../../lib/index');
var Plots = require('../../../src/plots/plots');
var Lib = require('../../../src/lib');
var modeBarButtons = require('../../../src/components/modebar/buttons');
var dfltConfig = require('../../../src/plot_api/plot_config').dfltConfig;

var d3Select = require('../../strict-d3').select;
var createGraphDiv = require('../assets/create_graph_div');
Expand Down Expand Up @@ -509,12 +510,43 @@ describe('config argument', function() {
modeBarButtons.sendChartToCloud.click(gd);
var msg = document.querySelector('.plotly-cloud-dialog-message');
expect(msg).not.toBe(null, 'confirmation dialog should be shown');
expect(msg.textContent).toContain('example.plotly.com');
expect(msg.textContent).toBe('This chart will be sent to example.plotly.com.');

// The host name is shown as a link to the server's origin,
// leaving off the endpoint path
var link = msg.querySelector('.plotly-cloud-dialog-message--hostname');
expect(link).not.toBe(null, 'host name should be shown as a link');
expect(link.textContent).toBe('example.plotly.com');
expect(link.getAttribute('href')).toBe('https://example.plotly.com');
})
.then(done, done.fail);
});

it('should NOT open confirmation dialog when set to an invalid URL', function(done) {
it('should show Plotly Cloud wording when left at the default URL', function(done) {
Plotly.newPlot(gd, [], {}, {})
.then(function() {
expect(gd._context.plotlyServerURL).toBe(dfltConfig.plotlyServerURL);
modeBarButtons.sendChartToCloud.click(gd);

var msg = document.querySelector('.plotly-cloud-dialog-message');
expect(msg).not.toBe(null, 'confirmation dialog should be shown');
expect(msg.textContent).toContain('This chart will be uploaded to Plotly Cloud to create a sharing link.');

var link = msg.querySelector('.plotly-cloud-dialog-message--hostname');
expect(link).not.toBe(null, 'Plotly Cloud should be shown as a link');
expect(link.textContent).toBe('Plotly Cloud');
expect(link.getAttribute('href')).toBe(new URL(dfltConfig.plotlyServerURL).origin);

var account = msg.querySelector('.plotly-cloud-dialog-message--account');
expect(account).not.toBe(null, 'account note should be shown');
expect(account.textContent).toContain('Plotly Cloud account');
})
.then(done, done.fail);
});

it('should NOT open confirmation dialog when set to an unparseable URL', function(done) {
var errorSpy = spyOn(console, 'error');

Plotly.newPlot(gd, [], {}, {
plotlyServerURL: 'dummy'
})
Expand All @@ -523,6 +555,21 @@ describe('config argument', function() {
modeBarButtons.sendChartToCloud.click(gd);
var msg = document.querySelector('.plotly-cloud-dialog-message');
expect(msg).toBe(null, 'confirmation dialog should not be shown');
expect(errorSpy).toHaveBeenCalledWith('Invalid plotlyServerURL: dummy');
})
.then(done, done.fail);
});

it('should NOT open confirmation dialog when set to a non-http(s) URL', function(done) {
var errorSpy = spyOn(console, 'error');

Plotly.newPlot(gd, [], {}, {
plotlyServerURL: 'ftp://example.plotly.com'
})
.then(function() {
modeBarButtons.sendChartToCloud.click(gd);
expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'confirmation dialog should not be shown');
expect(errorSpy).toHaveBeenCalledWith('Invalid protocol for plotlyServerURL: ftp://example.plotly.com');
})
.then(done, done.fail);
});
Expand All @@ -543,10 +590,46 @@ describe('config argument', function() {
// Should open the provided URL's origin in a new tab,
// adding the current page's origin as a query parameter
expect(openSpy).toHaveBeenCalledWith('https://example.plotly.com/endpoint?origin=http%3A%2F%2Flocalhost%3A9876', '_blank');

// Confirming should also dismiss the dialog
expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'dialog should be closed');
})
.then(done, done.fail);
});

[{
name: 'clicking cancel button',
dismiss: function() {
mouseEvent('click', 0, 0, {element: document.querySelector('.plotly-cloud-dialog-btn--cancel')});
}
}, {
name: 'clicking the backdrop',
dismiss: function() {
mouseEvent('click', 0, 0, {element: document.querySelector('.plotly-cloud-dialog')});
}
}, {
name: 'pressing Escape',
dismiss: function() {
document.dispatchEvent(new window.KeyboardEvent('keydown', {key: 'Escape'}));
}
}].forEach(function(spec) {
it('should close dialog without uploading when ' + spec.name, function(done) {
Plotly.newPlot(gd, [], {}, {
plotlyServerURL: 'https://example.plotly.com/endpoint'
})
.then(function() {
modeBarButtons.sendChartToCloud.click(gd);
expect(document.querySelector('.plotly-cloud-dialog')).not.toBe(null, 'dialog should be shown');

spec.dismiss();

expect(document.querySelector('.plotly-cloud-dialog')).toBe(null, 'dialog should be closed');
expect(openSpy).not.toHaveBeenCalled();
})
.then(done, done.fail);
});
});

it('has lesser priority than window env', function(done) {
window.PLOTLYENV = {BASE_URL: 'https://yo.plotly.com/endpoint'};

Expand Down