From ffe4611598779feea9b65fb5ce1576b1fe57c7f4 Mon Sep 17 00:00:00 2001 From: FancMa01 Date: Mon, 19 Aug 2024 12:41:01 -0600 Subject: [PATCH] Mfancher/login order (#826) * save progress * Finish Initial tutorial Added a tutorial when a user access' tombolo and doesn't have applications or clusters set up. Locked all components until these are set up. Guided tour with links to guides Once set up, unlocks rest of application * Initial tour finished Added back cluster tour on the cluster screen to work with new workflows. * remove finish buttons * store collapsed in local storage --- Tombolo/client-reactjs/src/App.js | 196 +++++++++++++-- .../src/components/admin/apps/Applications.js | 42 +++- .../admin/clusters/ClusterActionBtn.jsx | 46 ++-- .../src/components/admin/clusters/index.js | 40 ++- .../src/components/common/Constants.js | 13 +- .../src/components/layout/Header.js | 52 ++-- .../src/components/layout/LeftNav.js | 230 +++++++++++++----- .../src/redux/actions/Application.js | 42 +++- .../src/redux/reducers/ApplicationReducer.js | 40 ++- 9 files changed, 543 insertions(+), 158 deletions(-) diff --git a/Tombolo/client-reactjs/src/App.js b/Tombolo/client-reactjs/src/App.js index 5e09dc9b..079bb28d 100644 --- a/Tombolo/client-reactjs/src/App.js +++ b/Tombolo/client-reactjs/src/App.js @@ -1,6 +1,6 @@ import React, { Suspense } from 'react'; import { connect } from 'react-redux'; -import { Layout, ConfigProvider, Spin, Card } from 'antd'; +import { Layout, ConfigProvider, Spin, Card, Tour } from 'antd'; import { Router, Route, Switch } from 'react-router-dom'; import { Redirect } from 'react-router'; import history from './components/common/History'; @@ -71,6 +71,7 @@ import { PrivateRoute } from './components/common/PrivateRoute'; import { userActions } from './redux/actions/User'; import { checkBackendStatus } from './redux/actions/Backend'; import { store } from './redux/store/Store'; +import { applicationActions } from './redux/actions/Application'; const { Header, Content } = Layout; @@ -78,16 +79,23 @@ const BG_COLOR = ''; class App extends React.Component { state = { - collapsed: true, + collapsed: false, locale: 'en', + message: '', + tourOpen: false, + clusterTourOpen: false, + appLinkRef: React.createRef(), + clusterLinkRef: React.createRef(), }; componentDidMount() { //if status of backend hasn't been retrieved, check it if (!this.props.backendStatus.statusRetrieved) { + this.setState({ message: 'Connecting to...' }); store.dispatch(checkBackendStatus()); } else { if (!this.props.authWithAzure) { + this.setState({ message: 'Authenticating...' }); store.dispatch(userActions.validateToken()); } @@ -100,10 +108,35 @@ class App extends React.Component { i18next.changeLanguage(localStorage.getItem('i18nextLng')); } } + + //listen for clicks on the document to close tour if nav link is clicked + document.addEventListener('click', this.handleClick); } + handleClick = (e) => { + if (this.state.appLinkRef.current && this.state.appLinkRef.current.contains(e.target)) { + this.setState({ tourOpen: false }); + } + + if (this.state.clusterLinkRef.current && this.state.clusterLinkRef.current.contains(e.target)) { + this.setState({ clusterTourOpen: false }); + } + }; + + //function to handle tour shown close + handleTourShownClose = () => { + this.setState({ tourOpen: false }); + }; + + //function to handle tour shown close + handleClusterTourShownClose = () => { + this.setState({ clusterTourOpen: false }); + }; + onCollapse = (collapsed) => { this.setState({ collapsed }); + //set collapsed into local storage + localStorage.setItem('collapsed', collapsed); }; // Setting locale for antd components. @@ -126,6 +159,29 @@ class App extends React.Component { const isBackendStatusRetrieved = this.props.backendStatus.statusRetrieved; const isApplicationSet = this.props.application && this.props.application.applicationId !== '' ? true : false; + //if an application doesn't exist and the tour hasn't been shown, show the tour + if (this.props.noApplication.noApplication && !this.props.noApplication.firstTourShown && isBackendConnected) { + //if you're not already on the application page, show the left nav tour + if (window.location.pathname !== '/admin/applications') { + this.setState({ tourOpen: true }); + } + this.props.dispatch(applicationActions.updateApplicationLeftTourShown(true)); + } + + //if an application exists, but a cluster doesn't, show the cluster tour + if ( + this.props.application?.applicationId && + this.props.noClusters.noClusters && + !this.props.noClusters.firstTourShown + ) { + //if you're not already on the cluster page, show the left nav tour + if (window.location.pathname !== '/admin/clusters') { + this.setState({ clusterTourOpen: true }); + } + + this.props.dispatch(applicationActions.updateClustersLeftTourShown(true)); + } + const dataFlowComp = () => { let applicationId = this.props.application ? this.props.application.applicationId : ''; let applicationTitle = this.props.application ? this.props.application.applicationTitle : ''; @@ -141,13 +197,78 @@ class App extends React.Component { } }; + //steps for tour + const steps = [ + { + title: 'Welcome to Tombolo', + description: + 'There is some setup that we need to complete before being able to fully utilize Tombolo. We will unlock features as we move through this interactive tutorial.', + target: null, + }, + { + title: 'Applications', + description: ( + <> +

+ It looks like you have not set up an application yet. Applications are a necessary part of Tombolos basic + functions, and we must set one up before unlocking the rest of the application. Click on the navigation + element to head to the application management screen and set one up. +

+
+

+ If youre interested to read more about applications, head to our documentation page at{' '} + + https://hpcc-systems.github.io/Tombolo/docs/Quick-Start/application + +

+ + ), + placement: 'right', + arrow: true, + target: () => this.state.appLinkRef?.current, + nextButtonProps: { style: { display: 'none' }, disabled: true }, + prevButtonProps: { style: { display: 'none' }, disabled: true }, + }, + ]; + + const clusterSteps = [ + { + title: 'Clusters', + description: ( + <> +

+ Now that we have an application set up, we can connect to an hpcc systems cluster to unlock the rest of + the application. Click the navigation element to head to the cluster management screen and set one up. +

+
+

+ If youre interested to read more about Clusters, head to our documentation page at{' '} + + https://hpcc-systems.github.io/Tombolo/docs/Quick-Start/cluster + +

+ + ), + placement: 'right', + arrrow: true, + target: () => this.state.clusterLinkRef?.current, + nextButtonProps: { style: { display: 'none' }, disabled: true }, + }, + ]; + return ( }> - {/* don't load anything until backend connection is checked */} - {!isBackendConnected ? ( + {/* Go through loading sequence, first check if backend is connected and report with proper message */} + {!isBackendConnected || !this.props.authWithAzure || !this.props.user || !this.props.user.token ? (
- {isBackendStatusRetrieved ? ( + {!isBackendConnected && isBackendStatusRetrieved ? ( } style={{ width: '50%', textAlign: 'center' }}>

Tombolo has encountered a network issue, please refresh the page. If the issue persists, contact @@ -164,33 +285,59 @@ class App extends React.Component {

) : ( - +
+
+ +
+ +
+

{this.state.message}

+
+
)}
) : ( - /* if backend is connected, load the app */ + /* Now that everything is loaded, present the application */ <> - {this.props.user && this.props.user.token ? ( -
- } - /> -
- ) : null} +
+ } + /> +
+ <>} + /> + + {' '} { this.setApplications(data); + + // SHOW TOUR IF NO APPLICATIONS + if (!this.props.noApplication.addButtonTourShown && data.length === 0) { + this.setState({ showTour: true }); + this.props.dispatch(applicationActions.updateApplicationAddButtonTourShown(true)); + } }) .catch((error) => { console.log(error); @@ -95,11 +103,10 @@ class Applications extends Component { notification.open({ message: 'Application Removed', description: 'The application has been removed.', - onClick: () => { - console.log('Closed!'); - }, + onClick: () => {}, }); this.getApplications(); + this.props.dispatch(applicationActions.applicationDeleted(app_id)); }) .catch((error) => { @@ -109,7 +116,12 @@ class Applications extends Component { // ADD OR CREATE NEW APPLICATION handleAddApplication = () => { - this.setState({ showAddApplicationModal: true, selectedApplication: null, isCreatingNewApp: true }); + this.setState({ + showAddApplicationModal: true, + selectedApplication: null, + isCreatingNewApp: true, + showTour: false, + }); }; // CLOSE ADD APPLICATION MODAL @@ -175,8 +187,22 @@ class Applications extends Component { if (record.visibility !== 'Public' && record.creator === this.props.user.username) return true; }; + handleTourClose = () => { + this.setState({ showTour: false }); + }; + //JSX render() { + const steps = [ + { + title: 'Add Application', + description: 'Click here to add an application. After adding an application, we can move on to the next step. ', + placement: 'bottom', + arrow: true, + target: () => this.state.appAddButtonRef?.current, + nextButtonProps: { style: { display: 'none' }, disabled: true }, + }, + ]; const applicationColumns = [ { width: '2%', @@ -271,12 +297,13 @@ class Applications extends Component { - } /> +
{ setDisplayAddClusterModal(true); + setTourOpen(false); }; + + //Tour steps + const steps = [ + { + title: 'Add Cluster', + description: + 'Click here to add a Cluster from the whitelist file. Once you add a Cluster you will unlock the rest of the application and be on your way to managing and monitoring your data. ', + placement: 'bottom', + arrow: true, + target: () => addClusterButtonRef.current, + nextButtonProps: { style: { display: 'none' }, disabled: true }, + }, + ]; return ( - ( - - - Add New Cluster - - - )}> - - + <> + setTourOpen(false)}> + + ( + + + Add New Cluster + + + )}> + + + ); } diff --git a/Tombolo/client-reactjs/src/components/admin/clusters/index.js b/Tombolo/client-reactjs/src/components/admin/clusters/index.js index 194b0dd6..e9d0529a 100644 --- a/Tombolo/client-reactjs/src/components/admin/clusters/index.js +++ b/Tombolo/client-reactjs/src/components/admin/clusters/index.js @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { message } from 'antd'; - +import { useSelector, useDispatch } from 'react-redux'; +import { applicationActions } from '../../../redux/actions/Application'; import BreadCrumbs from '../../common/BreadCrumbs'; import ClusterActionBtn from './ClusterActionBtn'; import ClustersTable from './ClustersTable'; @@ -19,6 +20,12 @@ function Clusters() { const [displayEditClusterModal, setDisplayEditClusterModal] = useState(false); const [tombolo_instance_name, setTombolo_instance_name] = useState(null); + //tour management + const { applicationReducer } = useSelector((store) => store); + const addClusterButtonRef = useRef(null); + const [tourOpen, setTourOpen] = useState(false); + const dispatch = useDispatch(); + // Effects useEffect(() => { // Get all saved clusters @@ -51,9 +58,36 @@ function Clusters() { } })(); }, []); + + useEffect(() => { + //show tour if needed + if ( + applicationReducer.noClusters.noClusters && + applicationReducer.noClusters.firstTourShown && + !applicationReducer.noClusters.addButtonTourShown + ) { + setTourOpen(true); + dispatch(applicationActions.updateClustersAddButtonTourShown(true)); + } + }, [applicationReducer]); + + //when cluster state is adjusted, dispatch redux action to update clusters in redux + useEffect(() => { + dispatch(applicationActions.updateClusters(clusters)); + }, [clusters]); + return ( <> - } /> + + } + /> { return { value: application.id, display: application.title }; }); + if (applications && applications.length > 0) { this.setState({ applications }); //this.handleRef(); this.debouncedHandleRef(); - } else { - this.openHelpNotification(); + } + + if (applications.length === 0) { + this.props.dispatch(applicationActions.updateNoApplicationFound({ noApplication: true })); } }) .catch((error) => { console.log(error); - }); + }) + .finally(() => {}); } } @@ -143,15 +146,14 @@ class AppHeader extends Component { this.props.dispatch(applicationActions.getConsumers()); this.props.dispatch(applicationActions.getLicenses()); this.props.dispatch(applicationActions.getConstraints()); - // this.props.dispatch(applicationActions.getIntegrations(this.props.application.applicationId)); this.props.dispatch(applicationActions.getAllActiveIntegrations()); } - //if noClusters.noClusters prop is true, show the no cluster modal and dispatch action to reset the noClusters state - if (this.props.noClusters.noClusters && !this.props.noClusters.redirect) { - this.props.dispatch(applicationActions.updateNoClustersFound({ noClusters: true, redirect: true })); - this.setState({ isClusterModalVisible: true }); - } + // //if noClusters.noClusters prop is true, show the no cluster modal and dispatch action to reset the noClusters state + // if (this.props.noClusters.noClusters && !this.props.noClusters.redirect) { + // this.props.dispatch(applicationActions.updateNoClustersFound({ noClusters: true, redirect: true })); + // this.setState({ isClusterModalVisible: true }); + // } if (this.props.newApplication) { let applications = this.state.applications; @@ -242,26 +244,7 @@ class AppHeader extends Component { } } - openHelpNotification = () => { - const key = `open${Date.now()}`; - notification.open({ - message: 'Hello', - description: - 'Welcome ' + - this.props.user.firstName + - ' ' + - this.props.user.lastName + - '. Please make sure you check out the User Guide under Help option.', - key, - onClose: this.close(), - icon: , - top: 70, - }); - }; - - close = () => { - console.log('Notification was closed. Either the close button was clicked or duration time elapsed.'); - }; + close = () => {}; search(value) { this.props.history.push('/report/' + value); @@ -542,11 +525,6 @@ class AppHeader extends Component {

Tombolo v{process.env.REACT_APP_VERSION}

- ); } diff --git a/Tombolo/client-reactjs/src/components/layout/LeftNav.js b/Tombolo/client-reactjs/src/components/layout/LeftNav.js index 89ebe170..06edb12d 100644 --- a/Tombolo/client-reactjs/src/components/layout/LeftNav.js +++ b/Tombolo/client-reactjs/src/components/layout/LeftNav.js @@ -3,7 +3,6 @@ import { Link, withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; import { Layout, Menu, Typography } from 'antd'; import { - LoadingOutlined, DashboardOutlined, FileSearchOutlined, ClusterOutlined, @@ -19,13 +18,15 @@ import { import { hasEditPermission } from '../common/AuthUtil.js'; import Text from '../common/Text'; -function getItem(label, key, icon, children, type) { + +function getItem(label, key, icon, children, type, disabled) { return { key, icon, children, label, type, + disabled, }; } @@ -63,11 +64,19 @@ class LeftNav extends Component { this.setState({ current: options[key] }); } } + + //check local storage for collapsed preference + const collapsed = localStorage.getItem('collapsed'); + if (collapsed === 'true') { + this.props.onCollapse(true); + } } render() { const applicationId = this.props?.applicationId || ''; const integrations = this.props?.integrations || []; + const disabled = applicationId === '' ? true : false; + const clusterDisabled = this.props?.clusters?.length === 0 ? true : false; const asrActive = integrations.some((i) => i.name === 'ASR' && i.application_id === applicationId); @@ -82,33 +91,71 @@ class LeftNav extends Component { //get item structure //label, key, icon, children, type; + const urlPrefix = () => { + if (applicationId) return '/' + applicationId; + else return ''; + }; + const items = [ getItem( - - - Assets - , - + <> + {disabled || clusterDisabled ? ( + <> + + Assets{' '} + + ) : ( + + + Assets + + )} + , '1', - null + null, + null, + null, + clusterDisabled ), getItem( - - - Definitions - , - + <> + {disabled || clusterDisabled ? ( + <> + + Definitions + + ) : ( + + + Definitions + + )} + , '2', - null + null, + null, + null, + clusterDisabled ), getItem( - - - Job Execution - , - + <> + {disabled || clusterDisabled ? ( + <> + + Job Execution + + ) : ( + + + Job Execution + + )} + , '3', - null + null, + null, + null, + clusterDisabled ), getItem( <> @@ -126,7 +173,9 @@ class LeftNav extends Component { , '4a', null, - null + null, + null, + clusterDisabled ), getItem( @@ -136,7 +185,9 @@ class LeftNav extends Component { , '4b', null, - null + null, + null, + clusterDisabled ), getItem( @@ -146,7 +197,9 @@ class LeftNav extends Component { , '4c', null, - null + null, + null, + clusterDisabled ), getItem( @@ -156,7 +209,9 @@ class LeftNav extends Component { , '4d', null, - null + null, + null, + clusterDisabled ), getItem( @@ -166,7 +221,9 @@ class LeftNav extends Component { , '4e', null, - null + null, + null, + clusterDisabled ), asrActive ? getItem( @@ -177,10 +234,14 @@ class LeftNav extends Component { , '4f', null, - null + null, + null, + clusterDisabled ) : null, - ] + ], + null, + clusterDisabled ), getItem( <> @@ -222,18 +283,32 @@ class LeftNav extends Component { null ) : null, - ] + ], + null, + clusterDisabled ), ]; const settingItems = [ getItem( - - - Clusters - , + <> + {disabled ? ( + + + Clusters + + ) : ( + + + Clusters + + )} + , '6', - null + null, + null, + null, + clusterDisabled ), getItem( <> @@ -253,29 +328,55 @@ class LeftNav extends Component { null, null ), - ] + ], + null, + clusterDisabled ), getItem( - - - Github - , + <> + {disabled || clusterDisabled ? ( + <> + + Github + + ) : ( + + + Github + + )}{' '} + , '8', - null + null, + null, + null, + clusterDisabled ), getItem( - - - Collaborator - , + <> + {disabled || clusterDisabled ? ( + <> + + Collaborator + + ) : ( + + + Collaborator + + )} + , '9', - null + null, + null, + null, + clusterDisabled ), ]; const adminItems = [ getItem( - + Applications , @@ -283,21 +384,34 @@ class LeftNav extends Component { null ), getItem( - - - Integrations - , + <> + {disabled || clusterDisabled ? ( + <> + + Integrations + + ) : ( + + + Integrations + + )} + , '11', - null - ), - getItem( - - {this.props.isReportLoading ? : } - Compliance - , - '12', - null + null, + null, + null, + clusterDisabled ), + //TODO: Uncomment when compliance is ready + // getItem( + // + // {this.props.isReportLoading ? : } + // Compliance + // , + // '12', + // null + // ), ]; const onClick = (e) => { diff --git a/Tombolo/client-reactjs/src/redux/actions/Application.js b/Tombolo/client-reactjs/src/redux/actions/Application.js index fed51ccc..5e2da9c6 100644 --- a/Tombolo/client-reactjs/src/redux/actions/Application.js +++ b/Tombolo/client-reactjs/src/redux/actions/Application.js @@ -11,10 +11,14 @@ export const applicationActions = { getLicenses, getConstraints, updateConstraints, - updateNoClustersFound, - // getIntegrations, - // updateIntegrations, getAllActiveIntegrations, + updateApplicationAddButtonTourShown, + updateApplicationLeftTourShown, + updateNoApplicationFound, + updateNoClustersFound, + updateClustersAddButtonTourShown, + updateClustersLeftTourShown, + updateClusters, }; function applicationSelected(applicationId, applicationTitle) { @@ -45,28 +49,52 @@ function applicationDeleted(applicationId) { }; } +function updateNoApplicationFound(noApplication) { + return { type: Constants.NO_APPLICATION_FOUND, noApplication }; +} + +function updateApplicationLeftTourShown(shown) { + return { type: Constants.APPLICATION_LEFT_TOUR_SHOWN, shown }; +} + +function updateApplicationAddButtonTourShown(shown) { + return { type: Constants.APPLICATION_ADD_BUTTON_TOUR_SHOWN, shown }; +} + function getClusters() { return (dispatch) => { fetch('/api/hpcc/read/getClusters', { headers: authHeader() }) .then((response) => (response.ok ? response.json() : handleError(response))) .then((clusters) => { //if there are no clusters, set this to null for later checks + if (clusters.length === 0) { - dispatch({ type: Constants.NO_CLUSTERS_FOUND, noClusters: { redirect: false, noClusters: true } }); - } else { - dispatch({ type: Constants.NO_CLUSTERS_FOUND, noClusters: { redirect: false, noClusters: false } }); + dispatch({ type: Constants.NO_CLUSTERS_FOUND, noClusters: true }); + return; } - dispatch({ type: Constants.CLUSTERS_RETRIEVED, clusters }); + dispatch({ type: Constants.CLUSTERS_FOUND, clusters }); }) .catch(console.log); }; } +function updateClusters(clusters) { + return { type: Constants.CLUSTERS_FOUND, clusters }; +} + function updateNoClustersFound(noClusters) { return { type: Constants.NO_CLUSTERS_FOUND, noClusters }; } +function updateClustersLeftTourShown(shown) { + return { type: Constants.CLUSTERS_LEFT_TOUR_SHOWN, shown }; +} + +function updateClustersAddButtonTourShown(shown) { + return { type: Constants.CLUSTERS_ADD_BUTTON_TOUR_SHOWN, shown }; +} + function getConsumers() { return (dispatch) => { fetch('/api/consumer/consumers', { headers: authHeader() }) diff --git a/Tombolo/client-reactjs/src/redux/reducers/ApplicationReducer.js b/Tombolo/client-reactjs/src/redux/reducers/ApplicationReducer.js index 2397c203..b5432f18 100644 --- a/Tombolo/client-reactjs/src/redux/reducers/ApplicationReducer.js +++ b/Tombolo/client-reactjs/src/redux/reducers/ApplicationReducer.js @@ -2,11 +2,12 @@ import { Constants } from '../../components/common/Constants'; const initialState = { application: {}, + noApplication: { firstTourShown: false, addButtonTourShown: false, noApplication: false }, + noClusters: { firstTourShown: false, addButtonTourShown: false, noClusters: false }, newApplication: '', updatedApplication: '', deletedApplicationId: '', clusters: [], - noClusters: { redirect: false, noClusters: false }, consumers: [], licenses: [], constraints: [], @@ -44,16 +45,46 @@ export function applicationReducer(state = initialState, action) { application: currentApplication, deletedApplicationId: action.applicationId, }; - case Constants.CLUSTERS_RETRIEVED: + case Constants.NO_APPLICATION_FOUND: return { ...state, - clusters: action.clusters, + noApplication: { ...state.noApplication, noApplication: true }, + }; + + case Constants.APPLICATION_LEFT_TOUR_SHOWN: + return { + ...state, + noApplication: { ...state.noApplication, firstTourShown: true }, + }; + case Constants.APPLICATION_ADD_BUTTON_TOUR_SHOWN: + return { + ...state, + noApplication: { ...state.noApplication, addButtonTourShown: true }, }; + case Constants.NO_CLUSTERS_FOUND: return { ...state, - noClusters: action.noClusters, + noClusters: { ...state.noClusters, noClusters: true }, }; + + case Constants.CLUSTERS_LEFT_TOUR_SHOWN: + return { + ...state, + noClusters: { ...state.noClusters, firstTourShown: true }, + }; + case Constants.CLUSTERS_ADD_BUTTON_TOUR_SHOWN: + return { + ...state, + noClusters: { ...state.noClusters, addButtonTourShown: true }, + }; + + case Constants.CLUSTERS_FOUND: + return { + ...state, + clusters: action.clusters, + }; + case Constants.CONSUMERS_RETRIEVED: return { ...state, @@ -84,6 +115,7 @@ export function applicationReducer(state = initialState, action) { ...state, integrations: action.integrations, }; + default: return state; }