Enhanced Tour feature (#2082)

* Enhanced Tour feature

* Fix: #2182 - Tour steps getting skipped

* Fix: #2180 - Stat links deactivated on browser back from Tour route

* Addressing comments

* Addressing comments

* Adding license
This commit is contained in:
darth-coder00 2022-01-13 20:43:07 +05:30 committed by GitHub
parent ad362a31fd
commit b29fdd3ccc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 634 additions and 453 deletions

View File

@ -12,7 +12,7 @@
"directory": "openmetadata-ui/src/main/resources/ui"
},
"dependencies": {
"@deuex-solutions/react-tour": "^1.0.0",
"@deuex-solutions/react-tour": "^1.1.1",
"@deuex-solutions/redoc": "2.0.0-rc.31",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",

View File

@ -451,7 +451,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
</div>
)}
{activeTab === 2 && (
<div>
<div id="sampleDataDetails">
<SampleDataTable sampleData={getSampleDataWithType()} />
</div>
)}

View File

@ -127,9 +127,7 @@ const MyAssetStats: FunctionComponent<Props> = ({
className="tw-font-medium tw-pl-2"
data-testid={data.dataTestId}
to={data.link}>
<button
className="tw-text-grey-body hover:tw-text-primary-hover hover:tw-underline"
disabled={AppState.isTourOpen}>
<button className="tw-text-grey-body hover:tw-text-primary-hover hover:tw-underline">
{data.data}
</button>
</Link>

View File

@ -17,47 +17,34 @@ import { isEmpty } from 'lodash';
import { observer } from 'mobx-react';
import { Match } from 'Models';
import React, { useEffect, useState } from 'react';
import {
Link,
NavLink,
useHistory,
useLocation,
useRouteMatch,
} from 'react-router-dom';
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import appState from '../../AppState';
import { getVersion } from '../../axiosAPIs/miscAPI';
import {
getExplorePathWithSearch,
navLinkSettings,
ROUTES,
TOUR_SEARCH_TERM,
} from '../../constants/constants';
import { urlGitbookDocs, urlJoinSlack } from '../../constants/url.const';
import { CurrentTourPageType } from '../../enums/tour.enum';
import { useAuth } from '../../hooks/authHooks';
import { userSignOut } from '../../utils/AuthUtils';
import { addToRecentSearched } from '../../utils/CommonUtils';
import {
inPageSearchOptions,
isInPageSearchAllowed,
} from '../../utils/RouterUtils';
import { activeLink, normalLink } from '../../utils/styleconstant';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import PopOver from '../common/popover/PopOver';
import DropDown from '../dropdown/DropDown';
import { WhatsNewModal } from '../Modals/WhatsNewModal';
import { COOKIE_VERSION } from '../Modals/WhatsNewModal/whatsNewData';
import { ReactComponent as IconDefaultUserProfile } from './../../assets/svg/ic-default-profile.svg';
import SearchOptions from './SearchOptions';
import Suggestions from './Suggestions';
import NavBar from '../nav-bar/NavBar';
const cookieStorage = new CookieStorage();
const Appbar: React.FC = (): JSX.Element => {
const location = useLocation();
const history = useHistory();
const { isAuthenticatedRoute, isSignedIn, isFirstTimeUser, isAuthDisabled } =
useAuth(location.pathname);
const {
isAuthenticatedRoute,
isSignedIn,
isFirstTimeUser,
isAuthDisabled,
isTourRoute,
} = useAuth(location.pathname);
const match: Match | null = useRouteMatch({
path: ROUTES.EXPLORE_WITH_SEARCH,
});
@ -65,18 +52,15 @@ const Appbar: React.FC = (): JSX.Element => {
const [searchValue, setSearchValue] = useState(searchQuery);
const [isOpen, setIsOpen] = useState<boolean>(true);
const [isFeatureModalOpen, setIsFeatureModalOpen] = useState<boolean>(false);
const [searchIcon, setSearchIcon] = useState<string>('icon-searchv1');
const [version, setVersion] = useState<string>('');
const navStyle = (value: boolean) => {
if (value) return { color: activeLink };
return { color: normalLink };
const handleFeatureModal = (value: boolean) => {
setIsFeatureModalOpen(value);
};
const openModal = () => {
setIsFeatureModalOpen(true);
const handleSearchChange = (value: string) => {
setSearchValue(value);
};
const supportLinks = [
@ -156,6 +140,40 @@ const Appbar: React.FC = (): JSX.Element => {
);
};
const profileDropdown = [
{
name: getUserDisplayName(),
to: '',
disabled: false,
icon: <></>,
isText: true,
},
{
name: 'Logout',
to: '#/action-1',
disabled: false,
method: userSignOut,
},
];
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (e.key === 'Enter') {
setIsOpen(false);
addToRecentSearched(target.value);
history.push(
getExplorePathWithSearch(
target.value,
// this is for if user is searching from another page
location.pathname.startsWith(ROUTES.EXPLORE)
? appState.explorePageTab
: 'tables'
)
);
}
};
useEffect(() => {
setSearchValue(searchQuery);
}, [searchQuery]);
@ -183,217 +201,20 @@ const Appbar: React.FC = (): JSX.Element => {
return (
<>
{isAuthenticatedRoute && isSignedIn ? (
<div className="tw-h-16 tw-py-3 tw-border-b-2 tw-border-separator">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap tw-px-6 centered-layout">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap">
<NavLink id="openmetadata_logo" to="/">
<SVGIcons
alt="OpenMetadata Logo"
icon={Icons.LOGO}
width="90"
/>
</NavLink>
<div className="tw-ml-5">
<NavLink
className="tw-nav focus:tw-no-underline"
data-testid="appbar-item"
id="explore"
style={navStyle(location.pathname.startsWith('/explore'))}
to={{
pathname: '/explore/tables',
}}>
Explore
</NavLink>
<DropDown
dropDownList={navLinkSettings}
label="Settings"
type="link"
/>
</div>
</div>
<div
className="tw-flex-none tw-relative tw-justify-items-center tw-ml-auto"
data-testid="appbar-item">
<SVGIcons
alt="icon-search"
className="tw-absolute tw-block tw-z-10 tw-w-4 tw-h-4 tw-right-2.5 tw-top-2.5 tw-leading-8 tw-text-center tw-pointer-events-none"
icon={searchIcon}
/>
<input
autoComplete="off"
className="tw-relative search-grey tw-rounded tw-border tw-border-main focus:tw-outline-none tw-pl-2 tw-pt-2 tw-pb-1.5 tw-form-inputs"
data-testid="searchBox"
id="searchBox"
placeholder="Search for Table, Topics, Dashboards and Pipeline"
type="text"
value={searchValue || ''}
onBlur={() => setSearchIcon('icon-searchv1')}
onChange={(e) => {
setSearchValue(e.target.value);
}}
onFocus={() => setSearchIcon('icon-searchv1color')}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (e.key === 'Enter') {
setIsOpen(false);
// below code is for tour feature
if (location.pathname.includes(ROUTES.TOUR)) {
if (searchValue === TOUR_SEARCH_TERM) {
appState.currentTourPage =
CurrentTourPageType.EXPLORE_PAGE;
setSearchValue('');
}
return;
}
addToRecentSearched(target.value);
history.push(
getExplorePathWithSearch(
target.value,
// this is for if user is searching from another page
location.pathname.startsWith(ROUTES.EXPLORE)
? appState.explorePageTab
: 'tables'
)
);
}
}}
/>
{!location.pathname.includes(ROUTES.TOUR) &&
searchValue &&
(isInPageSearchAllowed(location.pathname) ? (
<SearchOptions
isOpen={isOpen}
options={inPageSearchOptions(location.pathname)}
searchText={searchValue}
selectOption={(text) => {
appState.inPageSearchText = text;
}}
setIsOpen={setIsOpen}
/>
) : (
<Suggestions
isOpen={isOpen}
searchText={searchValue}
setIsOpen={setIsOpen}
/>
))}
</div>
<div className="tw-flex tw-ml-auto tw-pl-36">
<button
className="tw-nav focus:tw-no-underline hover:tw-underline"
data-testid="whatsnew-modal"
onClick={openModal}>
<PopOver
position="bottom"
title="What's new?"
trigger="mouseenter">
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mr-1"
icon={Icons.WHATS_NEW}
width="20"
/>
</PopOver>
</button>
<button
className="tw-nav focus:tw-no-underline hover:tw-underline"
data-testid="tour">
<PopOver
position="bottom"
title="Take a tour"
trigger="mouseenter">
<Link to={ROUTES.TOUR}>
<SVGIcons
alt="tour icon"
className="tw-align-middle tw-mr-0.5"
icon={Icons.TOUR}
width="20"
/>
</Link>
</PopOver>
</button>
<div>
<DropDown
dropDownList={supportLinks}
icon={
<PopOver
position="bottom"
title="Help"
trigger="mouseenter">
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mt-0.5 tw-mr-1"
icon={Icons.HELP_CIRCLE}
width="20"
/>
</PopOver>
}
isDropDownIconVisible={false}
isLableVisible={false}
label="Need Help"
type="link"
/>
</div>
</div>
<div data-testid="dropdown-profile">
<DropDown
dropDownList={[
{
name: getUserDisplayName(),
to: '',
disabled: false,
icon: <></>,
isText: true,
},
{
name: 'Logout',
to: '#/action-1',
disabled: false,
method: userSignOut,
},
]}
icon={
<>
<PopOver
position="bottom"
title="Profile"
trigger="mouseenter">
{appState?.userDetails?.profile?.images?.image512 ? (
<div className="profile-image tw--mr-2">
<img
alt="user"
src={appState.userDetails.profile.images.image512}
/>
</div>
) : (
<IconDefaultUserProfile
className="tw--mr-2"
style={{
height: '22px',
width: '22px',
borderRadius: '50%',
}}
/>
)}
</PopOver>
</>
}
isDropDownIconVisible={false}
type="link"
/>
</div>
</div>
{isFeatureModalOpen && (
<WhatsNewModal
header="Whats new!"
onCancel={() => setIsFeatureModalOpen(false)}
/>
)}
</div>
{isAuthenticatedRoute && isSignedIn && !isTourRoute ? (
<NavBar
handleFeatureModal={handleFeatureModal}
handleKeyDown={handleKeyDown}
handleSearchBoxOpen={setIsOpen}
handleSearchChange={handleSearchChange}
isFeatureModalOpen={isFeatureModalOpen}
isSearchBoxOpen={isOpen}
pathname={location.pathname}
profileDropdown={profileDropdown}
searchValue={searchValue || ''}
settingDropdown={navLinkSettings}
supportDropdown={supportLinks}
/>
) : null}
</>
);

View File

@ -12,7 +12,7 @@
*/
import classNames from 'classnames';
import { isNil, lowerCase } from 'lodash';
import { camelCase, isNil } from 'lodash';
import React from 'react';
import { TITLE_FOR_NON_OWNER_ACTION } from '../../../constants/constants';
import { getCountBadge } from '../../../utils/CommonUtils';
@ -58,7 +58,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs, className = '' }: Props) => {
<button
className={getTabClasses(tab.position, activeTab)}
data-testid="tab"
id={lowerCase(tab.name)}
id={camelCase(tab.name)}
onClick={() => setActiveTab?.(tab.position)}>
<SVGIcons
alt={tab.icon.alt}
@ -77,7 +77,7 @@ const TabsPane = ({ activeTab, setActiveTab, tabs, className = '' }: Props) => {
<button
className={getTabClasses(tab.position, activeTab)}
data-testid="tab"
id={lowerCase(tab.name)}
id={camelCase(tab.name)}
key={tab.position}
onClick={() => setActiveTab?.(tab.position)}>
<SVGIcons

View File

@ -0,0 +1,29 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DropDownListItem } from '../dropdown/types';
export interface NavBarProps {
settingDropdown: DropDownListItem[];
supportDropdown: DropDownListItem[];
profileDropdown: DropDownListItem[];
searchValue: string;
isTourRoute?: boolean;
isFeatureModalOpen: boolean;
pathname: string;
isSearchBoxOpen: boolean;
handleSearchBoxOpen: (value: boolean) => void;
handleFeatureModal: (value: boolean) => void;
handleSearchChange: (value: string) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}

View File

@ -0,0 +1,221 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import { Link, NavLink } from 'react-router-dom';
import AppState from '../../AppState';
import { ROUTES } from '../../constants/constants';
import {
inPageSearchOptions,
isInPageSearchAllowed,
} from '../../utils/RouterUtils';
import { activeLink, normalLink } from '../../utils/styleconstant';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import SearchOptions from '../app-bar/SearchOptions';
import Suggestions from '../app-bar/Suggestions';
import PopOver from '../common/popover/PopOver';
import DropDown from '../dropdown/DropDown';
import { WhatsNewModal } from '../Modals/WhatsNewModal';
import { ReactComponent as IconDefaultUserProfile } from './../../assets/svg/ic-default-profile.svg';
import { NavBarProps } from './NavBar.interface';
const NavBar = ({
settingDropdown,
supportDropdown,
profileDropdown,
searchValue,
isFeatureModalOpen,
isTourRoute = false,
pathname,
isSearchBoxOpen,
handleSearchBoxOpen,
handleFeatureModal,
handleSearchChange,
handleKeyDown,
}: NavBarProps) => {
const [searchIcon, setSearchIcon] = useState<string>('icon-searchv1');
const navStyle = (value: boolean) => {
if (value) return { color: activeLink };
return { color: normalLink };
};
return (
<>
<div className="tw-h-16 tw-py-3 tw-border-b-2 tw-border-separator">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap tw-px-6 centered-layout">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap">
<NavLink id="openmetadata_logo" to="/">
<SVGIcons alt="OpenMetadata Logo" icon={Icons.LOGO} width="90" />
</NavLink>
<div className="tw-ml-5">
<NavLink
className="tw-nav focus:tw-no-underline"
data-testid="appbar-item"
id="explore"
style={navStyle(pathname.startsWith('/explore'))}
to={{
pathname: '/explore/tables',
}}>
Explore
</NavLink>
<DropDown
dropDownList={settingDropdown}
label="Settings"
type="link"
/>
</div>
</div>
<div
className="tw-flex-none tw-relative tw-justify-items-center tw-ml-auto"
data-testid="appbar-item">
<SVGIcons
alt="icon-search"
className="tw-absolute tw-block tw-z-10 tw-w-4 tw-h-4 tw-right-2.5 tw-top-2.5 tw-leading-8 tw-text-center tw-pointer-events-none"
icon={searchIcon}
/>
<input
autoComplete="off"
className="tw-relative search-grey tw-rounded tw-border tw-border-main focus:tw-outline-none tw-pl-2 tw-pt-2 tw-pb-1.5 tw-form-inputs"
data-testid="searchBox"
id="searchBox"
placeholder="Search for Table, Topics, Dashboards and Pipeline"
type="text"
value={searchValue}
onBlur={() => setSearchIcon('icon-searchv1')}
onChange={(e) => {
handleSearchChange(e.target.value);
}}
onFocus={() => setSearchIcon('icon-searchv1color')}
onKeyDown={handleKeyDown}
/>
{!isTourRoute &&
searchValue &&
(isInPageSearchAllowed(pathname) ? (
<SearchOptions
isOpen={isSearchBoxOpen}
options={inPageSearchOptions(pathname)}
searchText={searchValue}
selectOption={(text) => {
AppState.inPageSearchText = text;
}}
setIsOpen={handleSearchBoxOpen}
/>
) : (
<Suggestions
isOpen={isSearchBoxOpen}
searchText={searchValue}
setIsOpen={handleSearchBoxOpen}
/>
))}
</div>
<div className="tw-flex tw-ml-auto tw-pl-36">
<button
className="tw-nav focus:tw-no-underline hover:tw-underline"
data-testid="whatsnew-modal"
onClick={() => handleFeatureModal(true)}>
<PopOver
position="bottom"
title="What's new?"
trigger="mouseenter">
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mr-1"
icon={Icons.WHATS_NEW}
width="20"
/>
</PopOver>
</button>
<button
className="tw-nav focus:tw-no-underline hover:tw-underline"
data-testid="tour">
<PopOver
position="bottom"
title="Take a tour"
trigger="mouseenter">
<Link to={ROUTES.TOUR}>
<SVGIcons
alt="tour icon"
className="tw-align-middle tw-mr-0.5"
icon={Icons.TOUR}
width="20"
/>
</Link>
</PopOver>
</button>
<div>
<DropDown
dropDownList={supportDropdown}
icon={
<PopOver position="bottom" title="Help" trigger="mouseenter">
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mt-0.5 tw-mr-1"
icon={Icons.HELP_CIRCLE}
width="20"
/>
</PopOver>
}
isDropDownIconVisible={false}
isLableVisible={false}
label="Need Help"
type="link"
/>
</div>
</div>
<div data-testid="dropdown-profile">
<DropDown
dropDownList={profileDropdown}
icon={
<>
<PopOver
position="bottom"
title="Profile"
trigger="mouseenter">
{AppState?.userDetails?.profile?.images?.image512 ? (
<div className="profile-image tw--mr-2">
<img
alt="user"
src={AppState.userDetails.profile.images.image512}
/>
</div>
) : (
<IconDefaultUserProfile
className="tw--mr-2"
style={{
height: '22px',
width: '22px',
borderRadius: '50%',
}}
/>
)}
</PopOver>
</>
}
isDropDownIconVisible={false}
type="link"
/>
</div>
</div>
{isFeatureModalOpen && (
<WhatsNewModal
header="Whats new!"
onCancel={() => handleFeatureModal(false)}
/>
)}
</div>
</>
);
};
export default NavBar;

View File

@ -13,210 +13,14 @@
import ReactTutorial from '@deuex-solutions/react-tour';
import { observer } from 'mobx-react';
import { TourSteps } from 'Models';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import AppState from '../../AppState';
import { TOUR_SEARCH_TERM } from '../../constants/constants';
import { CurrentTourPageType } from '../../enums/tour.enum';
import { useTour } from '../../hooks/useTour';
import TourEndModal from '../Modals/TourEndModal/TourEndModal';
type Steps = {
content?: string | React.ReactNode;
actionType?: string;
position?: string | number[];
selector?: string;
userTypeText?: string;
waitTimer?: number;
};
const getSteps = (value: string) => {
const modifiedValue = value.substr(0, 6);
return [
{
content: () => (
<p>
Discover all your data assets in a single place with{' '}
<strong>OpenMetadata</strong>, a centralized metadata store.
Collaborate with your team and get a holistic picture of the data in
your organization.
</p>
),
position: [5, 360],
stepInteraction: false,
selector: '#assetStatsCount',
},
{
content: () => (
<p>
<strong>Activity Feeds</strong> help you understand how the data is
changing in your organization.
</p>
),
position: [540, 540],
selector: '#feedData',
stepInteraction: false,
},
{
content: () => (
<p>
Look up for matching data assets by &quot;name&quot;,
&quot;description&quot;, &quot;column name&quot;, and so on from the{' '}
<strong>search</strong> box.
</p>
),
position: [535, 70],
selector: '#searchBox',
},
{
content: () => (
<p>
In the search box, type <strong>&quot;{modifiedValue}&quot;</strong>.
Hit <strong>Enter.</strong>
</p>
),
actionType: 'enter',
userTypeText: modifiedValue,
position: [535, 70],
selector: '#searchBox',
beforeNext: () => {
AppState.currentTourPage = CurrentTourPageType.EXPLORE_PAGE;
},
},
{
beforePrev: () => {
AppState.currentTourPage = CurrentTourPageType.MY_DATA_PAGE;
},
content: () => (
<p>
From the <strong>&quot;Explore&quot;</strong> page, get to know the
details on the table entity, tier, usage, and database information
</p>
),
selector: '#tabledatacard0',
stepInteraction: false,
position: [550, 310],
},
{
content: () => (
<p>
Click on the <strong>title of the asset</strong> to view more details.
</p>
),
actionType: 'click',
selector: '#tabledatacard0Title',
position: [210, 190],
beforeNext: () => {
AppState.currentTourPage = CurrentTourPageType.DATASET_PAGE;
},
},
{
beforePrev: () => {
AppState.currentTourPage = CurrentTourPageType.EXPLORE_PAGE;
},
content: () => (
<p>
{' '}
Get to know the table <strong>schema.</strong> Add a description.
</p>
),
stepInteraction: false,
position: [540, 65],
arrowPosition: 'bottom',
selector: '#schemaDetails',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 1;
},
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 2;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Profiler&quot;</strong> tab.
</p>
),
position: [20, 240],
selector: '#profiler',
},
{
content: () => (
<p>
Discover assets with the <strong>Data Profiler</strong>. Get to know
the table usage stats, check for null values and duplicates, and
understand the column data distributions.
</p>
),
arrowPosition: 'bottom',
stepInteraction: false,
position: [530, 20],
selector: '#profilerDetails',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 2;
},
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 3;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Lineage&quot;</strong> tab
</p>
),
position: [140, 240],
selector: '#lineage',
},
{
content: () => (
<p>
With <strong>Lineage</strong>, trace the path of data across tables,
pipelines, & dashboards.
</p>
),
position: [530, 45],
stepInteraction: false,
arrowPosition: 'bottom',
selector: '#lineageDetails',
},
{
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 5;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Manage&quot;</strong> tab
</p>
),
position: [260, 240],
selector: '#manage',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 3;
},
content: () => (
<p>
From <strong>&quot;Manage&quot;</strong>, you can claim ownership, and
set the tiers.
</p>
),
position: [560, 60],
arrowPosition: 'bottom',
stepInteraction: false,
selector: '#manageTabDetails',
},
];
};
const Tour = () => {
const Tour = ({ steps }: { steps: TourSteps[] }) => {
const { isTourOpen, handleIsTourOpen } = useTour();
const [steps] = useState<Steps[]>(getSteps(TOUR_SEARCH_TERM));
const [showTourEndModal, setShowTourEndModal] = useState(false);
const history = useHistory();
@ -230,6 +34,7 @@ const Tour = () => {
<ReactTutorial
disableDotsNavigation
disableKeyboardNavigation
showCloseButton
showNumber
accentColor="#7147E8"
inViewThreshold={200}
@ -251,9 +56,10 @@ const Tour = () => {
}
maskColor="#302E36"
playTour={isTourOpen}
stepWaitTimer={100}
stepWaitTimer={300}
steps={steps}
onRequestClose={() => handleIsTourOpen(false)}
onRequestSkip={handleModalSubmit}
/>
) : null}

View File

@ -1,3 +1,16 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
export const mockFeedData = [

View File

@ -21,6 +21,7 @@ export const useAuth = (pathname = '') => {
pathname !== ROUTES.SIGNUP &&
pathname !== ROUTES.SIGNIN &&
pathname !== ROUTES.CALLBACK;
const isTourRoute = pathname === ROUTES.TOUR;
return {
isSigningIn: authProvider.signingIn,
@ -31,9 +32,10 @@ export const useAuth = (pathname = '') => {
!authProvider.signingIn &&
isEmpty(userDetails) &&
isEmpty(newUser),
isAuthenticatedRoute: isAuthenticatedRoute,
isAuthenticatedRoute,
isAuthDisabled: authDisabled,
isAdminUser: userDetails?.isAdmin,
isFirstTimeUser: !isEmpty(userDetails) && !isEmpty(newUser),
isTourRoute,
};
};

View File

@ -423,6 +423,15 @@ declare module 'Models' {
showLabel?: boolean;
};
export type TourSteps = {
content?: string | React.ReactNode;
actionType?: string;
position?: string | number[];
selector?: string;
userTypeText?: string;
waitTimer?: number;
};
export interface FormErrorData {
[key: string]: string | undefined;
}

View File

@ -1,6 +1,20 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { observer } from 'mobx-react';
import { LeafNodes, SearchResponse } from 'Models';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import AppState from '../../AppState';
import DatasetDetails from '../../components/DatasetDetails/DatasetDetails.component';
import { DatasetOwner } from '../../components/DatasetDetails/DatasetDetails.interface';
@ -8,7 +22,9 @@ import Explore from '../../components/Explore/Explore.component';
import { ExploreSearchData } from '../../components/Explore/explore.interface';
import MyData from '../../components/MyData/MyData.component';
import { MyDataProps } from '../../components/MyData/MyData.interface';
import NavBar from '../../components/nav-bar/NavBar';
import Tour from '../../components/tour/Tour';
import { ROUTES, TOUR_SEARCH_TERM } from '../../constants/constants';
import {
mockDatasetData,
mockFeedData,
@ -22,6 +38,7 @@ import {
} from '../../generated/entity/data/table';
import { TagLabel } from '../../generated/type/tagLabel';
import { useTour } from '../../hooks/useTour';
import { getSteps } from '../../utils/TourUtils';
const mockData = {
data: { hits: { hits: [] } },
@ -36,6 +53,7 @@ const exploreCount = {
};
const TourPage = () => {
const location = useLocation();
const { handleIsTourOpen } = useTour();
const [currentPage, setCurrentPage] = useState<CurrentTourPageType>(
AppState.currentTourPage
@ -47,11 +65,29 @@ const TourPage = () => {
AppState.activeTabforTourDatasetPage
);
const [explorePageCounts, setExplorePageCounts] = useState(exploreCount);
const [searchValue, setSearchValue] = useState('');
const handleCountChange = () => {
setExplorePageCounts(exploreCount);
};
const clearSearchTerm = () => {
setSearchValue('');
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
if (location.pathname.includes(ROUTES.TOUR)) {
if (searchValue === TOUR_SEARCH_TERM) {
AppState.currentTourPage = CurrentTourPageType.EXPLORE_PAGE;
clearSearchTerm();
}
return;
}
}
};
useEffect(() => {
handleIsTourOpen(true);
AppState.currentTourPage = CurrentTourPageType.MY_DATA_PAGE;
@ -164,7 +200,21 @@ const TourPage = () => {
return (
<div>
<Tour />
<NavBar
isTourRoute
handleFeatureModal={handleCountChange}
handleKeyDown={handleKeyDown}
handleSearchBoxOpen={handleCountChange}
handleSearchChange={(value) => setSearchValue(value)}
isFeatureModalOpen={false}
isSearchBoxOpen={false}
pathname={location.pathname}
profileDropdown={[]}
searchValue={searchValue}
settingDropdown={[]}
supportDropdown={[]}
/>
<Tour steps={getSteps(TOUR_SEARCH_TERM, clearSearchTerm)} />
{getCurrentPage(currentPage)}
</div>
);

View File

@ -0,0 +1,232 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import AppState from '../AppState';
import { CurrentTourPageType } from '../enums/tour.enum';
export const getSteps = (value: string, clearSearchTerm: () => void) => {
return [
{
content: () => (
<p>
Discover all your data assets in a single place with{' '}
<strong>OpenMetadata</strong>, a centralized metadata store.
Collaborate with your team and get a holistic picture of the data in
your organization.
</p>
),
position: [5, 360],
stepInteraction: false,
selector: '#assetStatsCount',
},
{
content: () => (
<p>
<strong>Activity Feeds</strong> help you understand how the data is
changing in your organization.
</p>
),
position: [540, 540],
selector: '#feedData',
stepInteraction: false,
},
{
content: () => (
<p>
Search for matching data assets by &quot;name&quot;,
&quot;description&quot;, &quot;column name&quot;, and so on from the{' '}
<strong>Search</strong> box.
</p>
),
position: [535, 70],
selector: '#searchBox',
stepInteraction: false,
beforeNext: clearSearchTerm,
},
{
beforePrev: clearSearchTerm,
content: () => (
<p>
In the search box, type <strong>&quot;{value}&quot;</strong>. Hit{' '}
<strong>Enter.</strong>
</p>
),
actionType: 'enter',
userTypeText: value,
position: [535, 70],
selector: '#searchBox',
beforeNext: () => {
clearSearchTerm();
AppState.currentTourPage = CurrentTourPageType.EXPLORE_PAGE;
},
},
{
beforePrev: () => {
AppState.currentTourPage = CurrentTourPageType.MY_DATA_PAGE;
},
content: () => (
<p>
From the <strong>&quot;Explore&quot;</strong> page, view a summary of
each asset, including: title, description, owner, tier (importance),
usage, and location.
</p>
),
selector: '#tabledatacard0',
stepInteraction: false,
position: [550, 310],
},
{
content: () => (
<p>
Click on the <strong>title of the asset</strong> to view more details.
</p>
),
actionType: 'click',
selector: '#tabledatacard0Title',
position: [210, 190],
beforeNext: () => {
AppState.currentTourPage = CurrentTourPageType.DATASET_PAGE;
},
},
{
beforePrev: () => {
AppState.currentTourPage = CurrentTourPageType.EXPLORE_PAGE;
},
content: () => (
<p>
{' '}
Get to know the table <strong>Schema</strong>, including column names
and data types as well as column descriptions and tags. You can even
view metadata for complex types such as structs.
</p>
),
stepInteraction: false,
position: [540, 23],
arrowPosition: 'bottom',
selector: '#schemaDetails',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 1;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Sample Data&quot;</strong> tab.
</p>
),
position: [70, 240],
selector: '#sampleData',
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 2;
},
},
{
arrowPosition: 'bottom',
content: () => (
<p>
Take a look at the <strong>Sample Data</strong> to get a feel for what
the table contains and how you might use it.
</p>
),
position: [530, 60],
selector: '#sampleDataDetails',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 2;
},
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 3;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Profiler&quot;</strong> tab.
</p>
),
position: [200, 240],
selector: '#profiler',
},
{
content: () => (
<p>
Discover assets with the <strong>Data Profiler</strong>. Get to know
the table usage stats, check for null values and duplicates, and
understand the column data distributions.
</p>
),
arrowPosition: 'bottom',
stepInteraction: false,
position: [530, 20],
selector: '#profilerDetails',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 3;
},
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 4;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Lineage&quot;</strong> tab
</p>
),
position: [320, 240],
selector: '#lineage',
},
{
content: () => (
<p>
With <strong>Lineage</strong>, trace the path of data across tables,
pipelines, & dashboards.
</p>
),
position: [530, 45],
stepInteraction: false,
arrowPosition: 'bottom',
selector: '#lineageDetails',
},
{
beforeNext: () => {
AppState.activeTabforTourDatasetPage = 6;
},
actionType: 'click',
content: () => (
<p>
Click on the <strong>&quot;Manage&quot;</strong> tab
</p>
),
position: [440, 240],
selector: '#manage',
},
{
beforePrev: () => {
AppState.activeTabforTourDatasetPage = 4;
},
content: () => (
<p>
From <strong>&quot;Manage&quot;</strong>, you can claim ownership, and
set the tier.
</p>
),
position: [560, 60],
arrowPosition: 'bottom',
stepInteraction: false,
selector: '#manageTabDetails',
},
];
};

View File

@ -996,10 +996,10 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@deuex-solutions/react-tour@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@deuex-solutions/react-tour/-/react-tour-1.0.0.tgz#5714dd38cf3f89fc781cc7f53965b08400e68a13"
integrity sha512-oSprYz81nejf3ZqgZAy6dgyuyEEujJvGs5b+YB2Pnfui9i0vyOEseTtIoamYV+eRTjBsPhwv2wUa7RkdcJUcSQ==
"@deuex-solutions/react-tour@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@deuex-solutions/react-tour/-/react-tour-1.1.1.tgz#ad96bf0bcd651916a6e44d5e9c3bdc60e5fac036"
integrity sha512-BIw1zlGprOEtCoGexdyQl97bvBdeZb9ZPcB6e90fV+x2s9h+YDeZwEC+qisTQodLkBwovSRO990K2Quc6p39Aw==
dependencies:
classnames "^2.2.6"
lodash "^4.17.15"