diff --git a/RELEASE.md b/RELEASE.md index 8da48896b5..81f3a63be9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,6 +7,11 @@ Please follow the established format: --> # Next release +## Major features and improvements +- Display published URLs. (#1907) + +## Bug fixes and other changes + # Release 9.1.0 ## Major features and improvements diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 27e7878878..a0a38b75ac 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -199,3 +199,39 @@ Cypress.Commands.add('__comparisonMode__', () => { cy.get(':nth-child(3) > .runs-list-card__checked').click(); cy.wait('@compareThreeRuns').its('response.statusCode').should('eq', 200); }); + +/** + * Custom command to fillout and submit the hosting shareable URL form + */ +Cypress.Commands.add( + '__setupAndSubmitShareableUrlForm__', + (bucketName, endpointName, primaryButtonNodeText) => { + // Intercept the network request to mock with a fixture + cy.__interceptRest__( + '/api/deploy', + 'POST', + '/mock/deploySuccessResponse.json' + ).as('publishRequest'); + + // Reload the page to ensure a fresh state + cy.reload(); + + // Open the deploy modal + cy.get('.pipeline-menu-button--deploy').click(); + + // Select the first hosting platform from the dropdown + cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); + cy.get('.shareable-url-modal .dropdown__options section div').eq(1).click(); + + // Fill in the form + cy.get('.shareable-url-modal [data-test="bucket_name"]').type(bucketName); + cy.get('.shareable-url-modal [data-test="endpoint_name"]').type( + endpointName + ); + + // Submit the form + cy.get('.shareable-url-modal__button-wrapper button') + .contains(primaryButtonNodeText) + .click(); + } +); diff --git a/cypress/tests/ui/flowchart/shareable-urls.cy.js b/cypress/tests/ui/flowchart/shareable-urls.cy.js index 68b7185742..76926073db 100644 --- a/cypress/tests/ui/flowchart/shareable-urls.cy.js +++ b/cypress/tests/ui/flowchart/shareable-urls.cy.js @@ -1,5 +1,10 @@ -describe('Shareable URLs', () => { - it('verifies that users can open the Deploy Kedro-Viz modal. #TC-52', () => { +describe('Shareable URLs with empty localStorage', () => { + beforeEach(() => { + // Clears localStorage before each test + cy.clearLocalStorage(); + }); + + it('verifies that users can open the Deploy Kedro-Viz modal if the localStorage is empty. #TC-52', () => { // Intercept the network request to mock with a fixture cy.__interceptRest__( '/api/package-compatibilities', @@ -10,7 +15,6 @@ describe('Shareable URLs', () => { // Action cy.reload(); cy.get('.pipeline-menu-button--deploy').click({ force: true }); - cy.get('[data-test="disclaimerButton"]').click({ force: true }); // Assert after action cy.get('.shareable-url-modal .modal__wrapper').contains( @@ -39,7 +43,6 @@ describe('Shareable URLs', () => { it('verifies that shareable url modal closes on close button click #TC-54', () => { // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal__button-wrapper button') .contains('Cancel') .click(); @@ -56,7 +59,6 @@ describe('Shareable URLs', () => { // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); // Assert after action @@ -72,13 +74,14 @@ describe('Shareable URLs', () => { // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); // Assert after action cy.get( '.shareable-url-modal [data-test=kedro-pipeline-selector] .dropdown__label span' ).contains(selectedPlatform); - cy.get('.shareable-url-modal input').should('have.value', ''); + cy.get( + '.shareable-url-modal .shareable-url-modal__input-wrapper input' + ).should('have.value', ''); cy.get('.shareable-url-modal__button-wrapper button') .contains(primaryButtonNodeText) .should('be.disabled'); @@ -89,14 +92,15 @@ describe('Shareable URLs', () => { // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); cy.get('.shareable-url-modal .dropdown__options section div') .first() .click(); // Assert after action - cy.get('.shareable-url-modal input').should('have.value', ''); + cy.get( + '.shareable-url-modal .shareable-url-modal__input-wrapper input' + ).should('have.value', ''); cy.get('.shareable-url-modal__button-wrapper button') .contains(primaryButtonNodeText) .should('be.disabled'); @@ -109,7 +113,6 @@ describe('Shareable URLs', () => { // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); cy.get('.shareable-url-modal .dropdown__options section div') .first() @@ -131,7 +134,6 @@ describe('Shareable URLs', () => { const primaryButtonNodeText = 'Publish'; // Action cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); cy.get('.shareable-url-modal .dropdown__options section div') .first() @@ -165,7 +167,6 @@ describe('Shareable URLs', () => { // Action cy.reload(); cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); cy.get('.shareable-url-modal .dropdown__options section div') .first() @@ -181,9 +182,7 @@ describe('Shareable URLs', () => { // Wait for the POST request to complete and check the mocked response cy.wait('@publishRequest').then((interception) => { // Assert after action - cy.get('.shareable-url-modal__result-url').contains( - interception.response.body.url - ); + cy.get('.url-box__result-url').contains(interception.response.body.url); }); }); @@ -191,8 +190,6 @@ describe('Shareable URLs', () => { const bucketName = 'myBucketName'; const endpointName = 'http://www.example.com'; const primaryButtonNodeText = 'Publish'; - const primaryButtonNodeTextVariant = 'Publish'; - const secondaryButtonNodeText = 'Link Settings'; // Intercept the network request to mock with a fixture cy.__interceptRest__( @@ -204,7 +201,6 @@ describe('Shareable URLs', () => { // Action cy.reload(); cy.get('.pipeline-menu-button--deploy').click(); - cy.get('[data-test="disclaimerButton"]').click(); cy.get('.shareable-url-modal [data-test=kedro-pipeline-selector]').click(); cy.get('.shareable-url-modal .dropdown__options section div') .first() @@ -217,23 +213,91 @@ describe('Shareable URLs', () => { .contains(primaryButtonNodeText) .click(); - // Wait for the POST request to complete - cy.wait('@publishRequest'); - - // Action - cy.get('.shareable-url-modal__button-wrapper button') - .contains(secondaryButtonNodeText) - .click(); - cy.get('.shareable-url-modal__button-wrapper button') - .contains(primaryButtonNodeTextVariant) - .click(); - // Wait for the POST request to complete and check the mocked response cy.wait('@publishRequest').then((interception) => { // Assert after action - cy.get('.shareable-url-modal__result-url').contains( - interception.response.body.url + cy.get('.url-box__result-url').contains(interception.response.body.url); + }); + }); +}); + +describe('Shareable URLs with valid localStorage', () => { + const bucketName = 'myBucketName'; + const endpointName = 'http://www.example.com'; + const secondBucketName = 'mySecondBucketName'; + const secondEndpointName = 'http://www.exampleNumber2.com'; + + it('verifies that users can open the Published Content Kedro-Viz modal with valid URL after published it succesfully. #TC-XX', () => { + cy.__setupAndSubmitShareableUrlForm__(bucketName, endpointName, 'Publish'); + + // Wait for the POST request to complete + cy.wait('@publishRequest').then(() => { + // Close the modal once it publishes succesfully + cy.get('body').click(0, 0); + + // Open the deploy modal again + cy.get('.pipeline-menu-button--deploy').click(); + cy.get('.shareable-url-modal .modal__wrapper').contains( + `Publish and Share Kedro-Viz` ); + cy.get('.url-box__result-url').contains(endpointName); + }); + }); + + it('verifies that after published to more than one platform, users can open the Published Content Kedro-Viz modal to select on different option. #TC-XX1', () => { + const fillFormAndSubmit = (bucketName, endpointName) => { + cy.get('.shareable-url-modal [data-test="bucket_name"]').clear(); + cy.get('.shareable-url-modal [data-test="bucket_name"]').type(bucketName); + cy.get('.shareable-url-modal [data-test="endpoint_name"]').clear(); + cy.get('.shareable-url-modal [data-test="endpoint_name"]').type( + endpointName + ); + cy.get('.shareable-url-modal__button-wrapper button') + .contains('Publish') + .click(); + }; + + const selectHostingPlatform = (index) => { + cy.get( + '.shareable-url-modal [data-test=kedro-pipeline-selector]' + ).click(); + cy.get('.shareable-url-modal .dropdown__options section div') + .eq(index) + .click(); + }; + + cy.__setupAndSubmitShareableUrlForm__(bucketName, endpointName, 'Publish'); + + // Wait for the POST request to complete + cy.wait('@publishRequest').then(() => { + // Close the modal once it publishes successfully + cy.get('body').click(0, 0); + // Open the deploy modal again + cy.get('.pipeline-menu-button--deploy').click(); + cy.get('.shareable-url-modal__published-action button').click(); + + // Select the second hosting platform from the dropdown + selectHostingPlatform(2); + + // Fill in the form with second option + fillFormAndSubmit(secondBucketName, secondEndpointName); + + // Close the modal once it publishes successfully + cy.get('body').click(0, 0); + + cy.get('.pipeline-menu-button--deploy').click(); + + cy.get( + '.shareable-url-modal__published-dropdown-wrapper [data-test=kedro-pipeline-selector]' + ).click(); + + cy.get( + '.shareable-url-modal__published-dropdown-wrapper .dropdown__options section div' + ) + .eq(1) + .click(); + + cy.get('.url-box__result-url').contains(secondEndpointName); }); }); }); diff --git a/src/components/icons/info.js b/src/components/icons/info.js new file mode 100644 index 0000000000..3c822d98a9 --- /dev/null +++ b/src/components/icons/info.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const InfoIcon = ({ className }) => ( + + + +); + +export default InfoIcon; diff --git a/src/components/shareable-url-modal/compatibility-error-view/compatibility-error-view.js b/src/components/shareable-url-modal/compatibility-error-view/compatibility-error-view.js new file mode 100644 index 0000000000..d728f9787f --- /dev/null +++ b/src/components/shareable-url-modal/compatibility-error-view/compatibility-error-view.js @@ -0,0 +1,19 @@ +import React from 'react'; +import Button from '../../ui/button'; + +const CompatibilityErrorView = ({ onClick }) => ( +
+ + + + +
+); + +export default CompatibilityErrorView; diff --git a/src/components/shareable-url-modal/error-view/error-view.js b/src/components/shareable-url-modal/error-view/error-view.js new file mode 100644 index 0000000000..cf36677301 --- /dev/null +++ b/src/components/shareable-url-modal/error-view/error-view.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Button from '../../ui/button'; + +const ErrorView = ({ onClick, responseError }) => ( +
+

Error message: {responseError}

+ +
+); + +export default ErrorView; diff --git a/src/components/shareable-url-modal/loading-view/loading-view.js b/src/components/shareable-url-modal/loading-view/loading-view.js new file mode 100644 index 0000000000..967366ae7e --- /dev/null +++ b/src/components/shareable-url-modal/loading-view/loading-view.js @@ -0,0 +1,10 @@ +import React from 'react'; +import LoadingIcon from '../../icons/loading'; + +const LoadingView = ({ isLoading }) => ( +
+ +
+); + +export default LoadingView; diff --git a/src/components/shareable-url-modal/main-view/main-view.js b/src/components/shareable-url-modal/main-view/main-view.js new file mode 100644 index 0000000000..c0c9fadfe2 --- /dev/null +++ b/src/components/shareable-url-modal/main-view/main-view.js @@ -0,0 +1,190 @@ +/* eslint-disable camelcase */ +import React from 'react'; +import classnames from 'classnames'; +import Dropdown from '../../ui/dropdown'; +import Button from '../../ui/button'; +import IconButton from '../../ui/icon-button'; +import InfoIcon from '../../icons/info'; +import Input from '../../ui/input'; +import MenuOption from '../../ui/menu-option'; +import Toggle from '../../ui/toggle'; +import { + hostingPlatforms, + KEDRO_VIZ_PUBLISH_AWS_DOCS_URL, + KEDRO_VIZ_PUBLISH_AZURE_DOCS_URL, + KEDRO_VIZ_PUBLISH_GCP_DOCS_URL, + KEDRO_VIZ_PUBLISH_DOCS_URL, +} from '../../../config'; + +const renderTextContent = (isPreviewEnabled, setIsPreviewEnabled) => { + return ( +
+
+ Publish and Share Kedro-Viz +
+

+ Prerequisites{' '} +

+

+ Deploying and hosting Kedro-Viz requires access keys or user + credentials, depending on the chosen service provider. To use this + feature, please add your access keys or credentials as environment + variables in your project. More information can be found in the{' '} + + documentation + + . +

+

+ Disclaimer{' '} +

+

+ Disclaimer Kedro-Viz contains preview data for multiple datasets. You + can enable or disable all previews when publishing Kedro-Viz. +

+
+ All dataset previews + setIsPreviewEnabled((prev) => !prev)} + /> +
+
+ ); +}; + +const MainView = ({ + handleModalClose, + handleSubmit, + inputValues, + isFormDirty, + onPlatformChange, + onBuckNameChange, + onEndpointChange, + setIsPreviewEnabled, + isPreviewEnabled, + visible, +}) => { + const { platform, bucket_name, endpoint } = inputValues || {}; + + return ( + <> +
+ {renderTextContent(isPreviewEnabled, setIsPreviewEnabled)} +
+

+ Please enter the required information below. +

+
+
+ Hosting platform +
+ + {Object.entries(hostingPlatforms).map(([value, label]) => ( + + ))} + +
+
+
Bucket name
+ +
+
+
+
+ Endpoint URL +
+ + The endpoint URL is the link to where your Kedro-Viz will be + hosted. For information on obtaining the endpoint URL, + please refer to the documentation for{' '} + + AWS + + ,{' '} + + Azure + + ,{' '} + + GCP + +

+ } + icon={InfoIcon} + /> +
+ +
+
+
+
+ + +
+ + ); +}; + +export default MainView; diff --git a/src/components/shareable-url-modal/published-view/published-view.js b/src/components/shareable-url-modal/published-view/published-view.js new file mode 100644 index 0000000000..dc4d93f13e --- /dev/null +++ b/src/components/shareable-url-modal/published-view/published-view.js @@ -0,0 +1,98 @@ +import React from 'react'; +import classnames from 'classnames'; +import UrlBox from '../url-box/url-box'; +import Button from '../../ui/button'; +import Dropdown from '../../ui/dropdown'; +import MenuOption from '../../ui/menu-option'; + +import { getFilteredPlatforms, handleResponseUrl } from '../utils'; + +const PublishedView = ({ + hostingPlatformLocalStorageVal, + hostingPlatforms, + onChange, + onCopyClick, + onRepublishClick, + platform, + showCopied, +}) => { + const platformsKeysFromLocalStorage = Object.keys( + hostingPlatformLocalStorageVal + ); + const platformsValFromLocalStorage = Object.values( + hostingPlatformLocalStorageVal + ); + + const url = platform + ? hostingPlatformLocalStorageVal[platform]['endpoint'] + : platformsValFromLocalStorage[0]['endpoint']; + + const filteredPlatforms = getFilteredPlatforms( + hostingPlatforms, + platformsKeysFromLocalStorage + ); + + const href = handleResponseUrl( + url, + platform || platformsValFromLocalStorage[0]['platform'] + ); + + return ( + <> +
+
+ Publish and Share Kedro-Viz +
+ {platformsKeysFromLocalStorage.length === 1 ? ( + + ) : ( +
+ + {Object.entries(filteredPlatforms).map(([value, label]) => ( + + ))} + + +
+ )} +
+
+

+ Republish Kedro-Viz to push new updates, +
+ or publish and host Kedro-Viz with a new link. +

+ +
+ + ); +}; + +export default PublishedView; diff --git a/src/components/shareable-url-modal/shareable-url-modal.js b/src/components/shareable-url-modal/shareable-url-modal.js index 9d58bee238..f65cdd07af 100644 --- a/src/components/shareable-url-modal/shareable-url-modal.js +++ b/src/components/shareable-url-modal/shareable-url-modal.js @@ -4,40 +4,26 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import { toggleShareableUrlModal } from '../../actions'; import { fetchPackageCompatibilities } from '../../utils'; +import { saveLocalStorage, loadLocalStorage } from '../../store/helpers'; import { - hostingPlatform, + hostingPlatforms, inputKeyToStateKeyMap, - KEDRO_VIZ_PUBLISH_DOCS_URL, - KEDRO_VIZ_PREVIEW_DATASETS_DOCS_URL, - KEDRO_VIZ_PUBLISH_AWS_DOCS_URL, - KEDRO_VIZ_PUBLISH_AZURE_DOCS_URL, - KEDRO_VIZ_PUBLISH_GCP_DOCS_URL, + localStorageShareableUrl, PACKAGE_FSSPEC, + shareableUrlMessages, } from '../../config'; - -import Button from '../ui/button'; -import CopyIcon from '../icons/copy'; -import Dropdown from '../ui/dropdown'; -import IconButton from '../ui/icon-button'; -import Input from '../ui/input'; -import LoadingIcon from '../icons/loading'; import Modal from '../ui/modal'; -import MenuOption from '../ui/menu-option'; -import Tooltip from '../ui/tooltip'; -import './shareable-url-modal.scss'; +import PublishedView from './published-view/published-view'; +import CompatibilityErrorView from './compatibility-error-view/compatibility-error-view'; +import MainView from './main-view/main-view'; +import LoadingView from './loading-view/loading-view'; +import ErrorView from './error-view/error-view'; +import SuccessView from './success-view/success-view'; +import { getDeploymentStateByType, handleResponseUrl } from './utils'; +import { deployViz } from '../../utils'; -const modalMessages = (status, info = '') => { - const messages = { - failure: 'Something went wrong. Please try again later.', - loading: 'Shooting your files through space. Sit tight...', - success: - 'The current version of Kedro-Viz has been published and hosted via the link below.', - incompatible: `Publishing Kedro-Viz is only supported with fsspec>=2023.9.0. You are currently on version ${info}.\n\nPlease upgrade fsspec to a supported version and ensure you're using Kedro 0.18.2 or above.`, - }; - - return messages[status]; -}; +import './shareable-url-modal.scss'; const ShareableUrlModal = ({ onToggleModal, visible }) => { const [deploymentState, setDeploymentState] = useState('default'); @@ -52,8 +38,12 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { const [responseError, setResponseError] = useState(null); const [showCopied, setShowCopied] = useState(false); const [compatibilityData, setCompatibilityData] = useState({}); - const [canUseShareableUrls, setCanUseShareableUrls] = useState(true); - const [isDisclaimerViewed, setIsDisclaimerViewed] = useState(false); + const [isCompatible, setIsCompatible] = useState(true); + const [showPublishedView, setShowPublishedView] = useState(false); + const [hostingPlatformLocalStorageVal, setHostingPlatformLocalStorageVal] = + useState(loadLocalStorage(localStorageShareableUrl) || {}); + const [publishedPlatformKey, setPublishedPlatformKey] = useState(undefined); + const [isPreviewEnabled, setIsPreviewEnabled] = useState(true); useEffect(() => { async function fetchPackageCompatibility() { @@ -66,7 +56,7 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { (pckg) => pckg.package_name === PACKAGE_FSSPEC ); setCompatibilityData(fsspecPackage); - setCanUseShareableUrls(fsspecPackage?.is_compatible || false); + setIsCompatible(fsspecPackage?.is_compatible || false); // User's fsspec package version isn't compatible, so set // the necessary state to reflect that in the UI. @@ -82,6 +72,38 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { fetchPackageCompatibility(); }, []); + const setStateForPublishedView = () => { + if (Object.keys(hostingPlatformLocalStorageVal).length > 0) { + setDeploymentState('published'); + setShowPublishedView(true); + // set the publishedPlatformKey as the first one from localStorage by default + setPublishedPlatformKey(Object.keys(hostingPlatformLocalStorageVal)[0]); + } + }; + + const setStateForMainViewWithPublishedContent = () => { + if (Object.keys(hostingPlatformLocalStorageVal).length > 0) { + setShowPublishedView(false); + setDeploymentState('default'); + + const populatedContent = + hostingPlatformLocalStorageVal[publishedPlatformKey]; + + setInputValues(populatedContent); + + setIsFormDirty({ + hasBucketName: true, + hasPlatform: true, + hasEndpoint: true, + }); + } + }; + + useEffect(() => { + setStateForPublishedView(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const onChange = (key, value) => { setIsFormDirty((prevState) => ({ ...prevState, @@ -94,23 +116,68 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { ); }; + const updateFormWithLocalStorageData = (platformKey) => { + // if the selected platform is stored in localStorage, populate the form with the stored data + if (hostingPlatformLocalStorageVal[platformKey]) { + const populatedContent = hostingPlatformLocalStorageVal[platformKey]; + + setInputValues(populatedContent); + setIsFormDirty({ + hasBucketName: true, + hasPlatform: true, + hasEndpoint: true, + }); + } else { + // if not, only set the platform and reset the rest + const emptyContent = { + platform: platformKey, + bucket_name: '', + endpoint: '', + }; + setInputValues(emptyContent); + setIsFormDirty({ + hasBucketName: false, + hasPlatform: true, + hasEndpoint: false, + }); + } + }; + + const updateLocalStorageState = () => { + const selectedHostingPlatformVal = {}; + if (hostingPlatforms.hasOwnProperty(inputValues.platform)) { + selectedHostingPlatformVal[inputValues.platform] = { ...inputValues }; + } + saveLocalStorage(localStorageShareableUrl, selectedHostingPlatformVal); + + // filtering out the pairs where the key is in selectedHostingPlatformVal + const localStorageExcludingSelectedPlatform = Object.fromEntries( + Object.entries(hostingPlatformLocalStorageVal).filter( + ([key]) => !(key in selectedHostingPlatformVal) + ) + ); + + // set the new state with selectedHostingPlatformVal as the first value and localStorageExcludingSelectedPlatform + const newState = { + ...selectedHostingPlatformVal, + ...localStorageExcludingSelectedPlatform, + }; + setHostingPlatformLocalStorageVal(newState); + }; + const handleSubmit = async () => { setDeploymentState('loading'); setIsLoading(true); + setShowPublishedView(false); try { - const request = await fetch('/api/deploy', { - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify(inputValues), - }); + const request = await deployViz(inputValues); const response = await request.json(); if (request.ok) { setResponseUrl(response.url); setDeploymentState('success'); + updateLocalStorageState(); } else { setResponseUrl(null); setResponseError(response.message || 'Error occurred!'); @@ -125,8 +192,8 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { } }; - const onCopyClick = () => { - window.navigator.clipboard.writeText(responseUrl); + const onCopyClick = (url) => { + window.navigator.clipboard.writeText(url); setShowCopied(true); setTimeout(() => { @@ -137,13 +204,20 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { const handleModalClose = () => { onToggleModal(false); if (deploymentState !== 'incompatible') { - setDeploymentState('default'); + // reset the state to default as long as the user's fsspec package version is compatible + // and there are nothing stored in localStorage + if (Object.keys(hostingPlatformLocalStorageVal).length === 0) { + setDeploymentState('default'); + } + + // if there are items stored in localStorage, display the published view + setStateForPublishedView(); } + setResponseError(null); setIsLoading(false); setResponseUrl(null); setInputValues({}); - setIsDisclaimerViewed(false); setIsFormDirty({ hasBucketName: false, hasPlatform: false, @@ -151,333 +225,85 @@ const ShareableUrlModal = ({ onToggleModal, visible }) => { }); }; - const getDeploymentStateByType = (type) => { - if (deploymentState === 'default') { - return null; - } - - if (type === 'title') { - return deploymentState === 'success' - ? 'Kedro-Viz Published and Hosted' - : 'Publish and Share Kedro-Viz'; - } - - return modalMessages(deploymentState, compatibilityData.package_version); - }; - - const handleResponseUrl = () => { - // If the URL does not start with http:// or https://, append http:// to avoid relative path issue for GCP platform. - if (!/^https?:\/\//.test(responseUrl) && inputValues.platform === 'gcp') { - const url = 'http://' + responseUrl; - return url; - } - return responseUrl; - }; - - const clearDisclaimerMessage = () => setIsDisclaimerViewed(true); - - const renderCompatibilityMessage = () => { - return !canUseShareableUrls ? ( -
- - - - -
- ) : null; - }; - - const renderSuccessContent = () => { - return responseUrl ? ( - <> -
-
Hosted link
-
- - {responseUrl} - - {window.navigator.clipboard && ( -
- - -
- )} -
-
-
- - -
- - ) : null; - }; - - const renderErrorContent = () => { - return responseError ? ( -
-

Error message: {responseError}

- -
- ) : null; - }; - - const renderDisclaimerContent = () => { - return ( -
-
- Disclaimer: Please note that Kedro-Viz contains preview data for - multiple datasets. If you wish to disable the preview when publishing - Kedro-Viz, please refer to{' '} - - the documentation - {' '} - on how to do so. -
-
- - -
-
- ); - }; - - const renderTextContent = () => { - return ( -
-
- Publish and Share Kedro-Viz -
-

- Prerequisite: Deploying and hosting Kedro-Viz requires access keys or - user credentials, depending on the chosen cloud provider. To use this - feature, please add your access keys or credentials as environment - variables in your Kedro project. More information can be found in{' '} - - docs - - . -

-

- Enter the required information and a hosted link will be generated. -

-

- For more information on obtaining the Endpoint URL, refer to{' '} - - AWS - - ,{' '} - - Azure - {' '} - and{' '} - - GCP - {' '} - docs. -

-
- ); - }; - - const renderLoadingContent = () => { - return isLoading ? ( -
- -
- ) : null; - }; - - const renderMainContent = () => { - return !isLoading && - !responseUrl && - canUseShareableUrls && - !responseError ? ( - <> -
- {renderTextContent()} -
-
-
- Hosting platform -
- { - onChange('platform', selectedPlatform.value); - }} - width={null} - > - {Object.entries(hostingPlatform).map(([value, label]) => ( - - ))} - -
-
-
- Bucket Name -
- onChange('bucket_name', value)} - placeholder="Enter name" - resetValueTrigger={visible} - size="small" - type="input" - dataTest={'bucket_name'} - /> -
-
-
- Endpoint Link -
- onChange('endpoint', value)} - placeholder="Enter url" - resetValueTrigger={visible} - size="small" - type="input" - dataTest={'endpoint_name'} - /> -
-
-
-
- - -
- - ) : null; - }; - - const { platform, bucket_name, endpoint } = inputValues || {}; + const { platform } = inputValues || {}; return ( - {renderCompatibilityMessage()} - {!isDisclaimerViewed && canUseShareableUrls ? ( - renderDisclaimerContent() + {!isCompatible ? ( + + ) : showPublishedView ? ( + { + onChange('platform', selectedPlatform.value); + setPublishedPlatformKey(selectedPlatform.value); + }} + onCopyClick={onCopyClick} + onRepublishClick={setStateForMainViewWithPublishedContent} + platform={platform} + showCopied={showCopied} + /> ) : ( <> - {renderMainContent()} - {renderLoadingContent()} - {renderErrorContent()} - {renderSuccessContent()} + {!isLoading && !responseUrl && !responseError && ( + { + updateFormWithLocalStorageData(selectedPlatform.value); + }} + onBuckNameChange={(value) => onChange('bucket_name', value)} + onEndpointChange={(value) => onChange('endpoint', value)} + setIsPreviewEnabled={setIsPreviewEnabled} + isPreviewEnabled={isPreviewEnabled} + visible={visible} + /> + )} + {isLoading && } + {responseError && ( + { + setDeploymentState('default'); + setIsLoading(false); + setResponseUrl(null); + setResponseError(null); + }} + responseError={responseError} + /> + )} + {responseUrl && ( + + )} )} diff --git a/src/components/shareable-url-modal/shareable-url-modal.scss b/src/components/shareable-url-modal/shareable-url-modal.scss index a03602afcc..337250382e 100644 --- a/src/components/shareable-url-modal/shareable-url-modal.scss +++ b/src/components/shareable-url-modal/shareable-url-modal.scss @@ -3,25 +3,33 @@ .kui-theme--light { --color-description-text: #{variables.$black-300}; --color-deployed-link: #{variables.$black-900}; + --color-preview-data-text: #{variables.$black-900}; --color-modal-bg-1: #{variables.$grey-100}; --color-modal-bg-2: #{variables.$grey-0}; --color-footer-border: #{variables.$grey-300}; --color-text-doc-link: #{variables.$black-800}; - --color-form-label: #{variables.$grey-700}; + --color-form-label: #{variables.$black-200}; --input-bg: #{variables.$white-100}; --input-shadow: #{variables.$white-800}; + --color-btn-bg: #{variables.$white-500}; + --dropdown-bg: #{variables.$white-0}; + --dropdown-bg-hovered: #{variables.$white-200}; } .kui-theme--dark { --color-description-text: #{variables.$white-900}; --color-deployed-link: #{variables.$white-0}; + --color-preview-data-text: #{variables.$white-0}; --color-modal-bg-1: #{variables.$slate-300}; --color-modal-bg-2: #{variables.$slate-100}; --color-footer-border: #{variables.$black-600}; --color-text-doc-link: #{variables.$white-600}; - --color-form-label: #{variables.$grey-500}; + --color-form-label: #{variables.$black-0}; --input-bg: #{variables.$slate-700}; --input-shadow: #{variables.$slate-900}; + --color-btn-bg: #{variables.$black-600}; + --dropdown-bg: #{variables.$slate-600}; + --dropdown-bg-hovered: #{variables.$slate-400}; } .shareable-url-modal { @@ -36,7 +44,7 @@ } .modal__title { - margin-bottom: 24px; + margin: 0 0 24px; } .modal__description { @@ -71,7 +79,17 @@ display: flex; } + &__form-wrapper-title { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + margin-bottom: 32px; + margin-top: 0; + } + &__content-title { + color: var(--color-deployed-link); font-size: 20px; font-style: normal; font-weight: 400; @@ -80,6 +98,7 @@ } &__content-description { + color: var(--color-description-text); font-size: 14px; font-style: normal; font-weight: 400; @@ -91,29 +110,58 @@ } } + &__content-description-title { + color: var(--color-deployed-link); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + margin-top: 0; + } + + &__content-description-title--disclaimer::before { + background: var(--color-divider); + content: ''; + display: block; + height: 1px; + margin: 24px 0; + } + + &__content-preview-dataset { + align-items: center; + color: var(--color-preview-data-text); + display: flex; + font-size: 14px; + font-style: normal; + font-weight: 400; + justify-content: space-between; + line-height: 20px; + margin-top: 12px; + } + + &__content-toggle { + margin: 0; + } + &__content-note { - margin-top: 18px; color: var(--color-form-label); + margin-top: 18px; a { color: var(--color-form-label); } } - &__paregraph-divider { - margin-bottom: 30px; - } - &__content-wrapper { - padding: 48px; background: var(--color-modal-bg-2); box-shadow: 0 0 16px 0 rgb(0 0 0 / 10%); + padding: 48px; } &__form-wrapper { - padding: 48px; background: var(--color-modal-bg-1); box-shadow: inset 0 0 16px 0 rgb(0 0 0 / 10%); + padding: 48px; } &__input-wrapper { @@ -137,6 +185,11 @@ text-decoration: none; border: 1px solid variables.$blue-300; } + + &::placeholder { + color: #{variables.$black-500}; + opacity: 1; + } } } @@ -148,6 +201,56 @@ color: var(--color-form-label); } + &__endpoint-url-wrapper { + align-items: center; + display: flex; + justify-content: space-between; + + .pipeline-icon--container { + list-style-type: none; + } + + .shareable-url-modal__input-label { + margin-bottom: 0; + } + + .shareable-url-modal__input-label-text { + color: inherit; + } + + .shareable-url-modal__information-icon { + height: 40px; + + .pipeline-icon { + right: 0; + fill: #{variables.$black-0}; + } + + .pipeline-toolbar__label__visible { + max-width: 180px; + text-align: left; + white-space: inherit; + width: 180px; + + &::after { + top: 5%; + } + } + + .pipeline-toolbar__label-right { + margin-left: 2em; + + .shareable-url-modal__information-text { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + margin: 0; + } + } + } + } + &__button-wrapper { align-items: baseline; display: flex; @@ -180,21 +283,6 @@ width: 100%; } - &__result { - margin-bottom: 48px; - width: 100%; - - .toolbox { - margin: 0 4px 0 20px; - } - - .pipeline-icon { - position: relative; - top: unset; - right: unset; - } - } - &__error { display: flex; flex-direction: column; @@ -210,7 +298,7 @@ } &__url-wrapper { - background: rgb(255 255 255 / 4%); + background-color: #{variables.$slate-700}; display: flex; height: 50px; justify-content: space-between; @@ -273,6 +361,99 @@ .menu-option__content { font-size: 14px; } + + .dropdown__placeholder { + color: #{variables.$black-500}; + } +} + +.shareable-url-modal__published-wrapper { + .modal__wrapper { + padding: 0; + width: 707px; + } +} + +.shareable-url-modal__published { + background-color: var(--color-modal-bg-1); + padding: 48px; + width: 100%; + + .shareable-url-modal__content-title { + padding-bottom: 32px; + } +} + +.shareable-url-modal__published-dropdown-wrapper { + display: flex; + flex-direction: row; + + .dropdown__label { + height: 100%; + width: 209px; + padding-left: 20px; + } +} + +// override the dropdown styles for both mainView and publishedView +.shareable-url-modal__input-wrapper, +.shareable-url-modal__published-dropdown-wrapper { + .menu-option { + background-color: var(--dropdown-bg); + padding-left: 20px; + + &:hover { + background-color: var(--dropdown-bg-hovered); + } + } + + .dropdown__options { + box-shadow: none; + } +} + +.shareable-url-modal__published-action { + align-items: baseline; + background: var(--color-modal-bg-1); + border-top: 1px solid var(--color-footer-border); + display: flex; + justify-content: space-between; + padding: 48px 48px 56px; + width: 100%; + + .button__btn--secondary { + background-color: var(--color-btn-bg); + } + + .shareable-url-modal__published-action-text { + font-size: 16px; + font-weight: 400; + line-height: 24px; + width: 424px; + } +} + +.shareable-url-modal__success-wrapper { + .modal__content { + background-color: var(--color-modal-bg-1); + } + + .modal__wrapper { + width: 642px; + padding: 48px; + } + + .modal__description { + margin-top: 0; + } + + .shareable-url-modal__result { + width: 100%; + + .url-box__result-url-wrapper { + width: 440px; + } + } } .shareable-url-timestamp { diff --git a/src/components/shareable-url-modal/shareable-url-modal.test.js b/src/components/shareable-url-modal/shareable-url-modal.test.js index aaa85a2905..44cb4ab0a5 100644 --- a/src/components/shareable-url-modal/shareable-url-modal.test.js +++ b/src/components/shareable-url-modal/shareable-url-modal.test.js @@ -5,7 +5,6 @@ import { setup } from '../../utils/state.mock'; describe('ShareableUrlModal', () => { it('renders without crashing', () => { const wrapper = setup.mount(); - wrapper.find('[data-test="disclaimerButton"]').simulate('click'); - expect(wrapper.find('.shareable-url-modal__input-wrapper').length).toBe(3); + expect(wrapper.exists()).toBe(true); }); }); diff --git a/src/components/shareable-url-modal/success-view/success-view.js b/src/components/shareable-url-modal/success-view/success-view.js new file mode 100644 index 0000000000..e2217d2999 --- /dev/null +++ b/src/components/shareable-url-modal/success-view/success-view.js @@ -0,0 +1,22 @@ +import React from 'react'; +import UrlBox from '../url-box/url-box'; + +const SuccessView = ({ + handleResponseUrl, + onClick, + responseUrl, + showCopied, +}) => { + return responseUrl ? ( +
+ +
+ ) : null; +}; + +export default SuccessView; diff --git a/src/components/shareable-url-modal/url-box/url-box.js b/src/components/shareable-url-modal/url-box/url-box.js new file mode 100644 index 0000000000..7c473aa8ee --- /dev/null +++ b/src/components/shareable-url-modal/url-box/url-box.js @@ -0,0 +1,49 @@ +import React from 'react'; +import classnames from 'classnames'; +import Tooltip from '../../ui/tooltip'; +import Button from '../../ui/button'; + +import './url-box.scss'; + +const UrlBox = ({ className, url, onCopyClick, href, showCopiedText }) => ( +
+
+ + {url} + +
+ {window.navigator.clipboard && ( +
+ + +
+ )} +
+); + +export default UrlBox; diff --git a/src/components/shareable-url-modal/url-box/url-box.scss b/src/components/shareable-url-modal/url-box/url-box.scss new file mode 100644 index 0000000000..b16547f433 --- /dev/null +++ b/src/components/shareable-url-modal/url-box/url-box.scss @@ -0,0 +1,81 @@ +@use '../../../styles/variables' as variables; + +.kui-theme--light { + --color-link: #{variables.$black-900}; + --color-divider: #{variables.$white-900}; + --color-bg: #{variables.$white-100}; + --color-bg-copied: #{variables.$black-900}; + --color-text-copied: #{variables.$white-0}; +} + +.kui-theme--dark { + --color-link: #{variables.$white-0}; + --color-divider: #{variables.$black-500}; + --color-bg: #{variables.$slate-700}; + --color-bg-copied: #{variables.$white-0}; + --color-text-copied: #{variables.$black-900}; +} + +.url-box__wrapper { + background-color: var(--color-bg); + display: flex; + height: 50px; + justify-content: space-between; + text-overflow: ellipsis; + width: 100%; +} + +.url-box__wrapper--half-width { + border-left: 1px solid var(--color-divider); + + .url-box__result-url-wrapper { + width: 300px; + } +} + +.url-box__result-url-wrapper { + align-items: center; + display: flex; + max-width: 510px; + width: 100%; +} + +.url-box__result-url { + align-items: center; + color: var(--color-link); + cursor: pointer; + display: block; + font-family: inherit; + font-size: 16px; + line-height: 32px; + overflow: hidden; + padding-left: 16px; + text-decoration: underline; + text-overflow: ellipsis; + white-space: nowrap; + width: 90%; +} + +.url-box___button { + align-items: center; + display: flex; + position: relative; + + .button__btn { + width: 100px; + padding: 13px 0; + } + + .button__btn--secondary::after { + display: none; + } +} + +.url-box__button-copied { + border: none; + + .button__btn { + background-color: var(--color-bg-copied); + color: var(--color-text-copied); + } +} diff --git a/src/components/shareable-url-modal/utils.js b/src/components/shareable-url-modal/utils.js new file mode 100644 index 0000000000..6e675d870a --- /dev/null +++ b/src/components/shareable-url-modal/utils.js @@ -0,0 +1,50 @@ +export const getFilteredPlatforms = (hostingPlatforms, platformsKeys) => { + const filteredPlatforms = {}; + platformsKeys.forEach((key) => { + if (hostingPlatforms.hasOwnProperty(key)) { + filteredPlatforms[key] = hostingPlatforms[key]; + } + }); + + return filteredPlatforms; +}; + +/** + * Gets the deployment state message based on the type requested. + * + * @param {string} type - The type of message to return ('title' or 'message'). + * @param {string} deploymentState - The current deployment state. + * @param {Object} compatibilityData - Data containing the package version. + * @param {Function} modalMessages - Function to get modal messages based on deployment state and package version. + * @returns {string|null} - The message based on the deployment state and type, or null for default/published states. + */ +export const getDeploymentStateByType = ( + type, + deploymentState, + compatibilityData, + modalMessages +) => { + // This is because the default and published view has its own style + if (deploymentState === 'default' || deploymentState === 'published') { + return null; + } + + if (type === 'title') { + return deploymentState === 'success' + ? 'Kedro-Viz successfully hosted and published' + : 'Publish and Share Kedro-Viz'; + } + + if (type === 'message') { + return modalMessages(deploymentState, compatibilityData.package_version); + } +}; + +export const handleResponseUrl = (responseUrl, platform) => { + // If the URL does not start with http:// or https://, append http:// to avoid relative path issue for GCP platform. + if (!/^https?:\/\//.test(responseUrl) && platform === 'gcp') { + const url = 'http://' + responseUrl; + return url; + } + return responseUrl; +}; diff --git a/src/components/ui/icon-button/icon-button.js b/src/components/ui/icon-button/icon-button.js index 20bf1def6a..9c2d32b55d 100644 --- a/src/components/ui/icon-button/icon-button.js +++ b/src/components/ui/icon-button/icon-button.js @@ -104,7 +104,7 @@ IconButton.propTypes = { dataHeapEvent: PropTypes.string, disabled: PropTypes.bool, icon: PropTypes.func, - labelText: PropTypes.string, + labelText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), // it takes a string or a JSX element onClick: PropTypes.func, visible: PropTypes.bool, }; diff --git a/src/components/ui/modal/modal.js b/src/components/ui/modal/modal.js index 0572f2882a..6eda0b9539 100644 --- a/src/components/ui/modal/modal.js +++ b/src/components/ui/modal/modal.js @@ -45,8 +45,8 @@ const Modal = ({ })} >
- {title &&
{title}
} - {message &&
{message}
} + {title &&

{title}

} + {message &&

{message}

} {children}
diff --git a/src/config.js b/src/config.js index 9de91e6263..d68b1f9655 100644 --- a/src/config.js +++ b/src/config.js @@ -4,6 +4,7 @@ export const localStorageName = 'KedroViz'; export const localStorageFlowchartLink = 'KedroViz-link-to-flowchart'; export const localStorageMetricsSelect = 'KedroViz-metrics-chart-select'; export const localStorageRunsMetadata = 'KedroViz-runs-metadata'; +export const localStorageShareableUrl = 'KedroViz-shareable-url'; export const linkToFlowchartInitialVal = { fromURL: null, @@ -149,12 +150,24 @@ export const datasetStatLabels = ['rows', 'columns', 'file_size']; export const statsRowLen = 33; -export const hostingPlatform = { +export const hostingPlatforms = { aws: 'Amazon Web Services', gcp: 'Google Cloud', azure: 'Microsoft Azure', }; +export const shareableUrlMessages = (status, info = '') => { + const messages = { + failure: 'Something went wrong. Please try again later.', + loading: 'Shooting your files through space. Sit tight...', + success: + 'The deployment has been successful and Kedro-Viz is hosted via the link below..', + incompatible: `Publishing Kedro-Viz is only supported with fsspec>=2023.9.0. You are currently on version ${info}.\n\nPlease upgrade fsspec to a supported version and ensure you're using Kedro 0.18.2 or above.`, + }; + + return messages[status]; +}; + export const inputKeyToStateKeyMap = { // eslint-disable-next-line camelcase bucket_name: 'hasBucketName', diff --git a/src/utils/index.js b/src/utils/index.js index c84615c78f..72fe34af97 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -227,6 +227,18 @@ export async function fetchPackageCompatibilities() { return request; } +export async function deployViz(inputValues) { + const request = await fetch('/api/deploy', { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify(inputValues), + }); + + return request; +} + const nodeTypeMapObj = { nodes: 'task', task: 'nodes',