mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-18 04:05:42 +00:00
Tour feature (#716)
* setup tour page and implemented react-tutorial * completd tour-feature implementation
This commit is contained in:
parent
be7437c820
commit
ec51fadf19
@ -17600,6 +17600,17 @@
|
|||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-tutorial": {
|
||||||
|
"version": "https://github.com/deuex-solutions/react-tutorial/tarball/master",
|
||||||
|
"integrity": "sha512-QFHGepStGMR15zt3lRTqZB3NT6AmpBEHkxM6hi3fW6R86Jgu5y2ZYVY0fu93KDsAou/1saTUtkSJjs0rzwDKRA==",
|
||||||
|
"requires": {
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"scroll-smooth": "^1.1.0",
|
||||||
|
"scrollparent": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"reactjs-localstorage": {
|
"reactjs-localstorage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/reactjs-localstorage/-/reactjs-localstorage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/reactjs-localstorage/-/reactjs-localstorage-1.0.1.tgz",
|
||||||
@ -18594,6 +18605,16 @@
|
|||||||
"compute-scroll-into-view": "^1.0.17"
|
"compute-scroll-into-view": "^1.0.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scroll-smooth": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scroll-smooth/-/scroll-smooth-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-68OUOXKN/ykM/Dbp4Lhza3O9QQUuW/c01WTsZzDOUyVgb1I5QjT/awOHCCbuYTSV1QnExUQ9w+KcxmVxlXIiAg=="
|
||||||
|
},
|
||||||
|
"scrollparent": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-cV1bnMV3YPsivczDvvtb/gaxoxc="
|
||||||
|
},
|
||||||
"select-hose": {
|
"select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||||
|
@ -80,7 +80,8 @@
|
|||||||
"stream-http": "^3.2.0",
|
"stream-http": "^3.2.0",
|
||||||
"styled-components": "^5.2.3",
|
"styled-components": "^5.2.3",
|
||||||
"tailwindcss": "^2.1.4",
|
"tailwindcss": "^2.1.4",
|
||||||
"to-arraybuffer": "^1.0.1"
|
"to-arraybuffer": "^1.0.1",
|
||||||
|
"react-tutorial": "https://github.com/deuex-solutions/react-tutorial/tarball/master"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "NODE_ENV=development BABEL_ENV=development webpack serve --config ./webpack.config.dev.js --env development",
|
"start": "NODE_ENV=development BABEL_ENV=development webpack serve --config ./webpack.config.dev.js --env development",
|
||||||
|
@ -36,6 +36,8 @@ class AppState {
|
|||||||
inPageSearchText = '';
|
inPageSearchText = '';
|
||||||
explorePageTab = 'tables';
|
explorePageTab = 'tables';
|
||||||
|
|
||||||
|
isTourOpen = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,7 @@ export const FirstTimeUserModal: FunctionComponent<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className="tw-text-primary-active"
|
className="tw-text-primary-active"
|
||||||
|
id="next"
|
||||||
size="regular"
|
size="regular"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@ -47,6 +47,7 @@ import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
|||||||
import DropDown from '../dropdown/DropDown';
|
import DropDown from '../dropdown/DropDown';
|
||||||
import { WhatsNewModal } from '../Modals/WhatsNewModal';
|
import { WhatsNewModal } from '../Modals/WhatsNewModal';
|
||||||
import { COOKIE_VERSION } from '../Modals/WhatsNewModal/whatsNewData';
|
import { COOKIE_VERSION } from '../Modals/WhatsNewModal/whatsNewData';
|
||||||
|
import Tour from '../tour/Tour';
|
||||||
import { ReactComponent as IconDefaultUserProfile } from './../../assets/svg/ic-default-profile.svg';
|
import { ReactComponent as IconDefaultUserProfile } from './../../assets/svg/ic-default-profile.svg';
|
||||||
import SearchOptions from './SearchOptions';
|
import SearchOptions from './SearchOptions';
|
||||||
import Suggestions from './Suggestions';
|
import Suggestions from './Suggestions';
|
||||||
@ -69,6 +70,7 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [version, setVersion] = useState<string>('');
|
const [version, setVersion] = useState<string>('');
|
||||||
|
|
||||||
const navStyle = (value: boolean) => {
|
const navStyle = (value: boolean) => {
|
||||||
if (value) return { color: activeLink };
|
if (value) return { color: activeLink };
|
||||||
|
|
||||||
@ -167,7 +169,7 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
<div className="tw-h-14 tw-py-2 tw-px-5 tw-border-b-2 tw-border-separator">
|
<div className="tw-h-14 tw-py-2 tw-px-5 tw-border-b-2 tw-border-separator">
|
||||||
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap">
|
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap">
|
||||||
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap tw-mr-auto">
|
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap tw-mr-auto">
|
||||||
<NavLink to="/">
|
<NavLink id="openmetadata_logo" to="/">
|
||||||
<SVGIcons
|
<SVGIcons
|
||||||
alt="OpenMetadata Logo"
|
alt="OpenMetadata Logo"
|
||||||
icon={Icons.LOGO_SMALL}
|
icon={Icons.LOGO_SMALL}
|
||||||
@ -180,6 +182,7 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
<span className="fa fa-search tw-absolute tw-block tw-z-10 tw-w-9 tw-h-8 tw-leading-8 tw-text-center tw-pointer-events-none tw-text-gray-400" />
|
<span className="fa fa-search tw-absolute tw-block tw-z-10 tw-w-9 tw-h-8 tw-leading-8 tw-text-center tw-pointer-events-none tw-text-gray-400" />
|
||||||
<input
|
<input
|
||||||
className="tw-relative search-grey tw-rounded tw-border tw-border-main tw-bg-body-main focus:tw-outline-none tw-pl-8 tw-py-1"
|
className="tw-relative search-grey tw-rounded tw-border tw-border-main tw-bg-body-main focus:tw-outline-none tw-pl-8 tw-py-1"
|
||||||
|
id="searchBox"
|
||||||
type="text"
|
type="text"
|
||||||
value={searchValue || ''}
|
value={searchValue || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -224,6 +227,7 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
<NavLink
|
<NavLink
|
||||||
className="tw-nav focus:tw-no-underline"
|
className="tw-nav focus:tw-no-underline"
|
||||||
data-testid="appbar-item"
|
data-testid="appbar-item"
|
||||||
|
id="explore"
|
||||||
style={navStyle(location.pathname.startsWith('/explore'))}
|
style={navStyle(location.pathname.startsWith('/explore'))}
|
||||||
to={{
|
to={{
|
||||||
pathname: '/explore',
|
pathname: '/explore',
|
||||||
@ -249,6 +253,20 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
<span>What's new</span>
|
<span>What's new</span>
|
||||||
</button>
|
</button>
|
||||||
|
<NavLink
|
||||||
|
className="tw-nav focus:tw-no-underline hover:tw-underline"
|
||||||
|
style={navStyle(location.pathname.startsWith('/explore'))}
|
||||||
|
to={{
|
||||||
|
pathname: '/tour',
|
||||||
|
}}>
|
||||||
|
<SVGIcons
|
||||||
|
alt="Doc icon"
|
||||||
|
className="tw-align-middle tw--mt-0.5 tw-mr-1"
|
||||||
|
icon={Icons.WHATS_NEW}
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
<span>Tour</span>
|
||||||
|
</NavLink>
|
||||||
<div>
|
<div>
|
||||||
<DropDown
|
<DropDown
|
||||||
dropDownList={supportLinks}
|
dropDownList={supportLinks}
|
||||||
@ -319,6 +337,7 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Tour />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -149,6 +149,7 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
<Link
|
<Link
|
||||||
className="tw-block tw-px-4 tw-py-2 tw-text-sm"
|
className="tw-block tw-px-4 tw-py-2 tw-text-sm"
|
||||||
data-testid="data-name"
|
data-testid="data-name"
|
||||||
|
id={fqdn.replace(/\./g, '')}
|
||||||
to={getEntityLink(index, fqdn)}
|
to={getEntityLink(index, fqdn)}
|
||||||
onClick={() => setIsOpen(false)}>
|
onClick={() => setIsOpen(false)}>
|
||||||
{name}
|
{name}
|
||||||
@ -265,7 +266,7 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
aria-labelledby="menu-button"
|
aria-labelledby="menu-button"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
className="tw-origin-top-right tw-absolute tw-z-10
|
className="tw-origin-top-right tw-absolute tw-z-10
|
||||||
tw-w-60 tw-mt-1 tw-rounded-md tw-shadow-lg
|
tw-w-60 tw-mt-1 tw-rounded-md tw-shadow-lg
|
||||||
tw-bg-white tw-ring-1 tw-ring-black tw-ring-opacity-5 focus:tw-outline-none"
|
tw-bg-white tw-ring-1 tw-ring-black tw-ring-opacity-5 focus:tw-outline-none"
|
||||||
role="menu">
|
role="menu">
|
||||||
{getEntitiesSuggestions()}
|
{getEntitiesSuggestions()}
|
||||||
|
@ -25,7 +25,9 @@ const TabsPane = ({ activeTab, setActiveTab, tabs }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tw-bg-transparent tw--mx-4">
|
<div className="tw-bg-transparent tw--mx-4">
|
||||||
<nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4">
|
<nav
|
||||||
|
className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4"
|
||||||
|
id="tabs">
|
||||||
{tabs.map((tab) =>
|
{tabs.map((tab) =>
|
||||||
tab.isProtected ? (
|
tab.isProtected ? (
|
||||||
<NonAdminAction
|
<NonAdminAction
|
||||||
|
@ -383,7 +383,7 @@ const EntityTable = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tw-table-responsive">
|
<div className="tw-table-responsive" id="schemaTable">
|
||||||
<table className="tw-w-full" {...getTableProps()}>
|
<table className="tw-w-full" {...getTableProps()}>
|
||||||
<thead>
|
<thead>
|
||||||
{/* eslint-disable-next-line */}
|
{/* eslint-disable-next-line */}
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactTutorial from 'react-tutorial';
|
||||||
|
import { searchData } from '../../axiosAPIs/miscAPI';
|
||||||
|
import { PAGE_SIZE } from '../../constants/constants';
|
||||||
|
import { SearchIndex } from '../../enums/search.enum';
|
||||||
|
import { useTour } from '../../hooks/useTour';
|
||||||
|
|
||||||
|
type Steps = {
|
||||||
|
content: string;
|
||||||
|
actionType: string;
|
||||||
|
position: string;
|
||||||
|
selector: string;
|
||||||
|
userTypeText?: string;
|
||||||
|
waitTimer?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSteps = (value: string) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
content: 'Click on the next.',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#next',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Click on the next.',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#next',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Click on Explore OpenMetadata.',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#take-tour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Click on explore.',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#explore',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: `Type "${value}" in search box.`,
|
||||||
|
actionType: 'typing',
|
||||||
|
userTypeText: value,
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#searchBox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Click on the table.',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#bigqueryshopifydim_address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'Understand the schema of the table and add description, Claim ownership. Add tags etc..',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#tabs',
|
||||||
|
actionType: 'wait',
|
||||||
|
waitTimer: 10000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Click here to explore more',
|
||||||
|
actionType: 'click',
|
||||||
|
position: 'bottom',
|
||||||
|
selector: '#openmetadata_logo',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tour = () => {
|
||||||
|
const { isTourOpen, handleIsTourOpen } = useTour();
|
||||||
|
const [steps, setSteps] = useState<Steps[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchData('', 1, PAGE_SIZE, '', '', '', SearchIndex.TABLE).then(
|
||||||
|
(res: AxiosResponse) => {
|
||||||
|
const table = res.data.hits.hits[0];
|
||||||
|
setSteps(getSteps(table._source.table_name));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isTourOpen ? (
|
||||||
|
<ReactTutorial
|
||||||
|
disableKeyboardNavigation
|
||||||
|
showNumber
|
||||||
|
maskColor="#302E36"
|
||||||
|
playTour={isTourOpen}
|
||||||
|
showButtons={false}
|
||||||
|
showNavigation={false}
|
||||||
|
steps={steps}
|
||||||
|
onRequestClose={() => handleIsTourOpen(false)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(Tour);
|
@ -100,6 +100,7 @@ export const ROUTES = {
|
|||||||
CALLBACK: '/callback',
|
CALLBACK: '/callback',
|
||||||
NOT_FOUND: '/404',
|
NOT_FOUND: '/404',
|
||||||
MY_DATA: '/my-data',
|
MY_DATA: '/my-data',
|
||||||
|
TOUR: '/tour',
|
||||||
REPORTS: '/reports',
|
REPORTS: '/reports',
|
||||||
EXPLORE: '/explore',
|
EXPLORE: '/explore',
|
||||||
EXPLORE_WITH_SEARCH: `/explore/${PLACEHOLDER_ROUTE_TAB}/${PLACEHOLDER_ROUTE_SEARCHQUERY}`,
|
EXPLORE_WITH_SEARCH: `/explore/${PLACEHOLDER_ROUTE_TAB}/${PLACEHOLDER_ROUTE_SEARCHQUERY}`,
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import AppState from '../AppState';
|
||||||
|
|
||||||
|
export const useTour = () => {
|
||||||
|
const [isTourOpen, setIsTourOpen] = useState<boolean>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsTourOpen(AppState.isTourOpen);
|
||||||
|
}, [AppState.isTourOpen]);
|
||||||
|
|
||||||
|
const handleIsTourOpen = (value: boolean) => {
|
||||||
|
AppState.isTourOpen = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTourOpen,
|
||||||
|
handleIsTourOpen,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FirstTimeUserModal } from '../../components/Modals/FirstTimeUserModal/FirstTimeUserModal';
|
||||||
|
import { useTour } from '../../hooks/useTour';
|
||||||
|
import MyDataPage from '../my-data';
|
||||||
|
|
||||||
|
const TourPage = () => {
|
||||||
|
const [showFirstTimeUserModal, setShowFirstTimeUserModal] = useState(true);
|
||||||
|
const { handleIsTourOpen } = useTour();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleIsTourOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFirstTimeUser = () => {
|
||||||
|
setShowFirstTimeUserModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MyDataPage />
|
||||||
|
{showFirstTimeUserModal && (
|
||||||
|
<FirstTimeUserModal
|
||||||
|
onCancel={() => setShowFirstTimeUserModal(true)}
|
||||||
|
onSave={handleFirstTimeUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TourPage;
|
@ -29,3 +29,4 @@ declare module 'react-slick';
|
|||||||
declare module 'slick-carousel';
|
declare module 'slick-carousel';
|
||||||
declare module 'react-table';
|
declare module 'react-table';
|
||||||
declare module 'recharts';
|
declare module 'recharts';
|
||||||
|
declare module 'react-tutorial';
|
||||||
|
@ -39,12 +39,14 @@ import SwaggerPage from '../pages/swagger';
|
|||||||
import TagsPage from '../pages/tags';
|
import TagsPage from '../pages/tags';
|
||||||
import TeamsPage from '../pages/teams';
|
import TeamsPage from '../pages/teams';
|
||||||
import MyTopicDetailPage from '../pages/topic-details';
|
import MyTopicDetailPage from '../pages/topic-details';
|
||||||
|
import TourPage from '../pages/tour-page';
|
||||||
import UsersPage from '../pages/users';
|
import UsersPage from '../pages/users';
|
||||||
import WorkflowsPage from '../pages/workflows';
|
import WorkflowsPage from '../pages/workflows';
|
||||||
const AuthenticatedAppRouter: FunctionComponent = () => {
|
const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
|
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
|
||||||
|
<Route exact component={TourPage} path={ROUTES.TOUR} />
|
||||||
<Route exact component={ReportsPage} path={ROUTES.REPORTS} />
|
<Route exact component={ReportsPage} path={ROUTES.REPORTS} />
|
||||||
<Route exact component={ExplorePage} path={ROUTES.EXPLORE} />
|
<Route exact component={ExplorePage} path={ROUTES.EXPLORE} />
|
||||||
<Route component={ExplorePage} path={ROUTES.EXPLORE_WITH_SEARCH} />
|
<Route component={ExplorePage} path={ROUTES.EXPLORE_WITH_SEARCH} />
|
||||||
|
@ -31,7 +31,7 @@ module.exports = {
|
|||||||
mode: 'development',
|
mode: 'development',
|
||||||
|
|
||||||
// Input configuration
|
// Input configuration
|
||||||
entry: path.join(__dirname, 'src/index.js'),
|
entry: ['@babel/polyfill', path.join(__dirname, 'src/index.js')],
|
||||||
|
|
||||||
// Output configuration
|
// Output configuration
|
||||||
output: {
|
output: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user