diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cb1c9850..0e19c01341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,13 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3395](https://github.com/plotly/dash/pull/3396) Add position argument to hooks.devtool - [#3403](https://github.com/plotly/dash/pull/3403) Add app_context to get_app, allowing to get the current app in routes. - [#3407](https://github.com/plotly/dash/pull/3407) Add `hidden` to callback arguments, hiding the callback from appearing in the devtool callback graph. +- [#3397](https://github.com/plotly/dash/pull/3397) Add optional callbacks, suppressing callback warning for missing component ids for a single callback. - [#3424](https://github.com/plotly/dash/pull/3424) Adds support for `Patch` on clientside callbacks class `dash_clientside.Patch`, as well as supporting side updates, eg: (Running, SetProps). - [#3347](https://github.com/plotly/dash/pull/3347) Added 'api_endpoint' to `callback` to expose api endpoints at the provided path for use to be executed directly without dash. +- [#3465](https://github.com/plotly/dash/pull/3465) Plotly cloud integrations, add devtool API, placeholder plotly cloud CLI & publish button, `dash[cloud]` extra dependencies. ## Fixed - [#3395](https://github.com/plotly/dash/pull/3395) Fix Components added through set_props() cannot trigger related callback functions. Fix [#3316](https://github.com/plotly/dash/issues/3316) -- [#3397](https://github.com/plotly/dash/pull/3397) Add optional callbacks, suppressing callback warning for missing component ids for a single callback. - [#3415](https://github.com/plotly/dash/pull/3415) Fix the error triggered when only a single no_update is returned for client-side callback functions with multiple Outputs. Fix [#3366](https://github.com/plotly/dash/issues/3366) - [#3416](https://github.com/plotly/dash/issues/3416) Fix DeprecationWarning in dash/_jupyter.py by migrating from deprecated ipykernel.comm.Comm to comm module diff --git a/dash/_plotly_cli.py b/dash/_plotly_cli.py new file mode 100644 index 0000000000..9974cd23b9 --- /dev/null +++ b/dash/_plotly_cli.py @@ -0,0 +1,15 @@ +import sys + + +def cli(): + try: + from plotly_cloud.cli import main # pylint: disable=import-outside-toplevel + + main() + except ImportError: + print( + "Plotly cloud is not installed," + " install it with `pip install dash[cloud]` to use the plotly command", + file=sys.stderr, + ) + sys.exit(-1) diff --git a/dash/background_callback/managers/diskcache_manager.py b/dash/background_callback/managers/diskcache_manager.py index 094485ad71..9c939306df 100644 --- a/dash/background_callback/managers/diskcache_manager.py +++ b/dash/background_callback/managers/diskcache_manager.py @@ -287,10 +287,14 @@ async def async_run(): } }, ) + if asyncio.iscoroutine(user_callback_output): user_callback_output = await user_callback_output if not errored: - cache.set(result_key, user_callback_output) + try: + cache.set(result_key, user_callback_output) + except Exception as err: # pylint: disable=broad-except + print(f"Diskcache manager couldn't save output: {err}") if asyncio.iscoroutinefunction(fn): func = partial(ctx.run, async_run) diff --git a/dash/dash-renderer/src/components/error/icons/CloudSlashIcon.svg b/dash/dash-renderer/src/components/error/icons/CloudSlashIcon.svg new file mode 100644 index 0000000000..a3b62a12df --- /dev/null +++ b/dash/dash-renderer/src/components/error/icons/CloudSlashIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash/dash-renderer/src/components/error/icons/ErrorIcon.svg b/dash/dash-renderer/src/components/error/icons/ErrorIcon.svg index 6ba3a3735a..91398f8256 100644 --- a/dash/dash-renderer/src/components/error/icons/ErrorIcon.svg +++ b/dash/dash-renderer/src/components/error/icons/ErrorIcon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/dash/dash-renderer/src/components/error/icons/GraphIcon.svg b/dash/dash-renderer/src/components/error/icons/GraphIcon.svg index 0e4cf42f29..870a16a732 100644 --- a/dash/dash-renderer/src/components/error/icons/GraphIcon.svg +++ b/dash/dash-renderer/src/components/error/icons/GraphIcon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/dash/dash-renderer/src/components/error/menu/DebugMenu.css b/dash/dash-renderer/src/components/error/menu/DebugMenu.css index 78b71d9f0e..c0643d07db 100644 --- a/dash/dash-renderer/src/components/error/menu/DebugMenu.css +++ b/dash/dash-renderer/src/components/error/menu/DebugMenu.css @@ -112,7 +112,7 @@ } .dash-debug-menu:hover { - background-color: #108de4; + background-color: #7f4bc4; } .dash-debug-menu__outer { @@ -208,13 +208,12 @@ align-items: center; transition: background-color 0.2s; font-family: Verdana, sans-serif !important; - font-weight: bold; - color: black; + color: #7f4bc4; } .dash-debug-menu__button.dash-debug-menu__button--selected { color: #7f4bc4; - box-shadow: 0 2px #0071c2; + box-shadow: 0 2px #7f4bc4; } .dash-debug-menu__button.dash-debug-menu__button--selected:hover { color: #5806c4; @@ -253,3 +252,9 @@ font-size: 14px; margin-left: 3px; } +.dash-debug-menu__icon { + color: #9ca3af; +} +.dash-debug-menu__button:hover .dash-debug-menu__icon { + color: #5806c4; +} diff --git a/dash/dash-renderer/src/components/error/menu/DebugMenu.react.js b/dash/dash-renderer/src/components/error/menu/DebugMenu.react.js index 5395251d7e..0a432d0af1 100644 --- a/dash/dash-renderer/src/components/error/menu/DebugMenu.react.js +++ b/dash/dash-renderer/src/components/error/menu/DebugMenu.react.js @@ -1,6 +1,7 @@ import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import {concat} from 'ramda'; +import {useSelector} from 'react-redux'; import './DebugMenu.css'; @@ -14,7 +15,8 @@ import {VersionInfo} from './VersionInfo.react'; import {CallbackGraphContainer} from '../CallbackGraph/CallbackGraphContainer.react'; import {FrontEndErrorContainer} from '../FrontEnd/FrontEndErrorContainer.react'; import ExternalWrapper from '../../../wrapper/ExternalWrapper'; -import {useSelector} from 'react-redux'; +import PlotlyCloud from './PlotlyCloud'; +import {DevtoolProvider, useDevtool} from './DevtoolContext'; const classes = (base, variant, variant2) => `${base} ${base}--${variant}` + (variant2 ? ` ${base}--${variant2}` : ''); @@ -75,6 +77,7 @@ const MenuContent = ({ return (
{custom && <>{custom.left}} + {!config.plotly_cloud_installed ? : null}
@@ -122,8 +135,8 @@ const MenuContent = ({ ); }; -const DebugMenu = ({error, hotReload, config, children}) => { - const [popup, setPopup] = useState('errors'); +const Debug = ({error, hotReload, config, children}) => { + const {popup, setPopup} = useDevtool(); const [collapsed, setCollapsed] = useState(isCollapsed); const errCount = error.frontEnd.length + error.backEnd.length; @@ -154,6 +167,12 @@ const DebugMenu = ({error, hotReload, config, children}) => { const errors = concat(error.frontEnd, error.backEnd); + useEffect(() => { + if (error !== null && popup !== 'errors') { + setPopup('errors'); + } + }, [error]); + const popupContent = (
{popup == 'callbackGraph' ? : undefined} @@ -207,6 +226,14 @@ const DebugMenu = ({error, hotReload, config, children}) => { ); }; +const DebugMenu = ({children, ...props}) => { + return ( + + {children} + + ); +}; + DebugMenu.propTypes = { children: PropTypes.object, error: PropTypes.object, diff --git a/dash/dash-renderer/src/components/error/menu/DevtoolContext.js b/dash/dash-renderer/src/components/error/menu/DevtoolContext.js new file mode 100644 index 0000000000..4faa360e96 --- /dev/null +++ b/dash/dash-renderer/src/components/error/menu/DevtoolContext.js @@ -0,0 +1,36 @@ +import React, {useContext, useMemo, useState} from 'react'; + +export const DevtoolContext = React.createContext({}); + +export const DevtoolProvider = ({children}) => { + const [popup, setPopup] = useState(''); + + return ( + + {children} + + ); +}; + +export const useDevtool = () => { + return useContext(DevtoolContext); +}; + +export const useDevtoolMenuButtonClassName = popupName => { + const {popup} = useDevtool(); + + const className = useMemo(() => { + const base = 'dash-debug-menu__button'; + if (popup === popupName) { + return base + ' dash-debug-menu__button--selected'; + } + return base; + }, [popup]); + + return className; +}; diff --git a/dash/dash-renderer/src/components/error/menu/PlotlyCloud.css b/dash/dash-renderer/src/components/error/menu/PlotlyCloud.css new file mode 100644 index 0000000000..c412999864 --- /dev/null +++ b/dash/dash-renderer/src/components/error/menu/PlotlyCloud.css @@ -0,0 +1,88 @@ +/* + Same as in plotly-cloud but without the publish + in the name to avoid future conflicts if changed + upstream. +*/ +.plotly-cloud-modal-overlay { + position: absolute; + bottom: 100%; + left: -1px; + z-index: 10000; +} + +.plotly-cloud-modal-content { + border-radius: 8px; + width: 522px; + background: white; + border: 1px solid #d1d5db; + border-radius: 4px 4px 0 0; + box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +.plotly-cloud-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e5e5e5; + background: #f9fafb; +} + +.plotly-cloud-modal-header h3 { + margin: 0; + color: #333; +} + +.plotly-cloud-modal-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.plotly-cloud-modal-close:hover { + color: #333; +} + +.plotly-cloud-modal-body { + padding: 20px; + color: black !important; + font-weight: 100; +} +button.plotly-cloud-modal-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px !important; + border-radius: 4px !important; + font-size: 12px !important; + font-weight: 600 !important; + cursor: pointer; + border: 1px solid transparent; + background: #8b5cf6; + color: white; + border-color: #7c3aed; +} + +.plotly-cloud-modal-button:disabled { + cursor: not-allowed; +} +.plotly-cloud-copy-install { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; + margin-top: 8px; +} diff --git a/dash/dash-renderer/src/components/error/menu/PlotlyCloud.js b/dash/dash-renderer/src/components/error/menu/PlotlyCloud.js new file mode 100644 index 0000000000..532ce96fa0 --- /dev/null +++ b/dash/dash-renderer/src/components/error/menu/PlotlyCloud.js @@ -0,0 +1,78 @@ +import React, {useMemo} from 'react'; + +import {useDevtool, useDevtoolMenuButtonClassName} from './DevtoolContext'; + +import './PlotlyCloud.css'; +import CloudSlashIcon from '../icons/CloudSlashIcon.svg'; + +const CLOUD_POPUP = 'cloud'; + +const PlotlyCloud = () => { + const {popup, setPopup} = useDevtool(); + + const className = useDevtoolMenuButtonClassName(CLOUD_POPUP); + + const isOpen = useMemo(() => popup === CLOUD_POPUP, [popup]); + + const onClick = () => { + if (popup === CLOUD_POPUP) { + setPopup(''); + } else { + setPopup(CLOUD_POPUP); + } + }; + + const onCopy = () => { + navigator.clipboard.writeText('pip install "dash[cloud]"'); + }; + + return ( + <> + + {isOpen ? ( +
+
+
+

Plotly Cloud

+ +
+
+
+ Install the extension to publish to Plotly + Cloud. +
+
+ {'pip install "dash[cloud]"'} + +
+
+
+
+ ) : null} + + ); +}; + +export default PlotlyCloud; diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts index 75365f731c..f77b2f2e17 100644 --- a/dash/dash-renderer/src/dashApi.ts +++ b/dash/dash-renderer/src/dashApi.ts @@ -4,6 +4,11 @@ import {getPath} from './actions/paths'; import {getStores} from './utils/stores'; import ExternalWrapper from './wrapper/ExternalWrapper'; import {stringifyId} from './actions/dependencies'; +import { + DevtoolContext, + useDevtool, + useDevtoolMenuButtonClassName +} from './components/error/menu/DevtoolContext'; /** * Get the dash props from a component path or id. @@ -34,5 +39,10 @@ function getLayout(componentPathOrId: string[] | string): any { DashContext, useDashContext, getLayout, - stringifyId + stringifyId, + devtool: { + DevtoolContext, + useDevtool, + useDevtoolMenuButtonClassName + } }; diff --git a/dash/dash.py b/dash/dash.py index 8430259c27..676fa0f8f4 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -639,6 +639,8 @@ def __init__( # pylint: disable=too-many-statements ) self.setup_startup_routes() + self._plotly_cloud = None + def _setup_hooks(self): # pylint: disable=import-outside-toplevel,protected-access from ._hooks import HooksManager @@ -926,6 +928,16 @@ def _config(self): "ddk_version": ddk_version, "plotly_version": plotly_version, } + if self._plotly_cloud is None: + try: + # pylint: disable=C0415,W0611 + import plotly_cloud # noqa: F401 + + self._plotly_cloud = True + except ImportError: + self._plotly_cloud = False + + config["plotly_cloud_installed"] = self._plotly_cloud if not self.config.serve_locally: config["plotlyjs_url"] = self._plotlyjs_url if self._dev_tools.hot_reload: diff --git a/requirements/cloud.txt b/requirements/cloud.txt new file mode 100644 index 0000000000..a8bbb9fdfb --- /dev/null +++ b/requirements/cloud.txt @@ -0,0 +1 @@ +plotly-cloud diff --git a/setup.py b/setup.py index 7ed781c20d..bdbec7b1cb 100644 --- a/setup.py +++ b/setup.py @@ -35,14 +35,16 @@ def read_req_file(req_type): "testing": read_req_file("testing"), "celery": read_req_file("celery"), "diskcache": read_req_file("diskcache"), - "compress": read_req_file("compress") + "compress": read_req_file("compress"), + "cloud": read_req_file("cloud"), }, entry_points={ "console_scripts": [ "dash-generate-components = " "dash.development.component_generator:cli", "renderer = dash.development.build_process:renderer", - "dash-update-components = dash.development.update_components:cli" + "dash-update-components = dash.development.update_components:cli", + "plotly = dash._plotly_cli:cli" ], "pytest11": ["dash = dash.testing.plugin"], }, diff --git a/tests/background_callback/utils.py b/tests/background_callback/utils.py index c6386f2680..1cefd4ecc3 100644 --- a/tests/background_callback/utils.py +++ b/tests/background_callback/utils.py @@ -150,6 +150,8 @@ def setup_background_callback_app(manager_name, app_name): for job in manager.running_jobs: manager.terminate_job(job) + time.sleep(1) + shutil.rmtree(cache_directory, ignore_errors=True) os.environ.pop("LONG_CALLBACK_MANAGER") os.environ.pop("DISKCACHE_DIR")