Adding welcome first user modal and auto-silent-renew for token (#184)

* Adding welcome first user modal and auto-silent-renew for token

* Minor fix
This commit is contained in:
darth-coder00 2021-08-16 08:44:48 +05:30 committed by GitHub
parent b5ed3feac2
commit d170cfe1c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 160 additions and 90 deletions

View File

@ -0,0 +1,19 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
// Module declaration to allow importing JPEG files
declare module '*.jpeg';

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -19,7 +19,7 @@ import { AxiosResponse } from 'axios';
import { CookieStorage } from 'cookie-storage'; import { CookieStorage } from 'cookie-storage';
import { isEmpty, isNil } from 'lodash'; import { isEmpty, isNil } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { User } from 'Models'; import { NewUser, User } from 'Models';
import { UserManager, WebStorageStateStore } from 'oidc-client'; import { UserManager, WebStorageStateStore } from 'oidc-client';
import React, { import React, {
ComponentType, ComponentType,
@ -28,7 +28,13 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { Callback, makeAuthenticator, makeUserManager } from 'react-oidc'; import { Callback, makeAuthenticator, makeUserManager } from 'react-oidc';
import { Redirect, Route, Switch, useHistory } from 'react-router-dom'; import {
Redirect,
Route,
Switch,
useHistory,
useLocation,
} from 'react-router-dom';
import appState from '../AppState'; import appState from '../AppState';
import axiosClient from '../axiosAPIs'; import axiosClient from '../axiosAPIs';
import { fetchAuthorizerConfig } from '../axiosAPIs/miscAPI'; import { fetchAuthorizerConfig } from '../axiosAPIs/miscAPI';
@ -38,6 +44,7 @@ import {
getUserByName, getUserByName,
getUsers, getUsers,
} from '../axiosAPIs/userAPI'; } from '../axiosAPIs/userAPI';
import { FirstTimeUserModal } from '../components/Modals/FirstTimeUserModal/FirstTimeUserModal';
import { import {
API_RES_MAX_SIZE, API_RES_MAX_SIZE,
oidcTokenKey, oidcTokenKey,
@ -72,9 +79,16 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
childComponentType, childComponentType,
children, children,
}: AuthProviderProps) => { }: AuthProviderProps) => {
const location = useLocation();
const history = useHistory(); const history = useHistory();
const showToast = useToastContext(); const showToast = useToastContext();
const { isSignedIn, isSigningIn, isSignedOut } = useAuth(); const {
isAuthenticatedRoute,
isFirstTimeUser,
isSignedIn,
isSigningIn,
isSignedOut,
} = useAuth(location.pathname);
const oidcUserToken = cookieStorage.getItem(oidcTokenKey); const oidcUserToken = cookieStorage.getItem(oidcTokenKey);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -82,11 +96,11 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
{} as UserManager {} as UserManager
); );
const [userManagerConfig, setUserManagerConfig] = useState< const [userManagerConfig, setUserManagerConfig] = useState<
Record<string, string | WebStorageStateStore> Record<string, string | boolean | WebStorageStateStore>
>({}); >({});
const clearOidcUserData = ( const clearOidcUserData = (
userConfig: Record<string, string | WebStorageStateStore> userConfig: Record<string, string | boolean | WebStorageStateStore>
): void => { ): void => {
cookieStorage.removeItem( cookieStorage.removeItem(
`oidc.user:${userConfig.authority}:${userConfig.client_id}` `oidc.user:${userConfig.authority}:${userConfig.client_id}`
@ -198,6 +212,15 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
}); });
}; };
const handleFirstTourModal = (skip: boolean) => {
appState.newUser = {} as NewUser;
if (skip) {
history.push(ROUTES.HOME);
} else {
// TODO: Route to tour page
}
};
useEffect(() => { useEffect(() => {
fetchAuthConfig(); fetchAuthConfig();
@ -241,39 +264,47 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
return ( return (
<> <>
{!loading ? ( {!loading ? (
<Switch> <>
<Route exact path={ROUTES.HOME}> <Switch>
{!isSignedIn && !isSigningIn ? ( <Route exact path={ROUTES.HOME}>
<Redirect to={ROUTES.SIGNIN} /> {!isSignedIn && !isSigningIn ? (
<Redirect to={ROUTES.SIGNIN} />
) : (
<Redirect to={ROUTES.MY_DATA} />
)}
</Route>
<Route exact component={PageNotFound} path={ROUTES.NOT_FOUND} />
{!isSigningIn ? (
<Route exact component={SigninPage} path={ROUTES.SIGNIN} />
) : null}
<Route
path={ROUTES.CALLBACK}
render={() => (
<Callback
userManager={userManager}
onSuccess={(user) => {
cookieStorage.setItem(oidcTokenKey, user.id_token, {
expires: getOidcExpiry(),
});
fetchUserByEmail(user as OidcUser);
}}
/>
)}
/>
{isSignedOut ? <Redirect to={ROUTES.SIGNIN} /> : null}
{oidcUserToken || !userManagerConfig?.client_id ? (
children
) : ( ) : (
<Redirect to={ROUTES.MY_DATA} /> <AppWithAuth />
)} )}
</Route> </Switch>
<Route exact component={PageNotFound} path={ROUTES.NOT_FOUND} /> {isAuthenticatedRoute && isFirstTimeUser ? (
{!isSigningIn ? ( <FirstTimeUserModal
<Route exact component={SigninPage} path={ROUTES.SIGNIN} /> onCancel={() => handleFirstTourModal(true)}
onSave={() => handleFirstTourModal(false)}
/>
) : null} ) : null}
<Route </>
path={ROUTES.CALLBACK}
render={() => (
<Callback
userManager={userManager}
onSuccess={(user) => {
cookieStorage.setItem(oidcTokenKey, user.id_token, {
expires: getOidcExpiry(),
});
fetchUserByEmail(user as OidcUser);
}}
/>
)}
/>
{isSignedOut ? <Redirect to={ROUTES.SIGNIN} /> : null}
{oidcUserToken || !userManagerConfig?.client_id ? (
children
) : (
<AppWithAuth />
)}
</Switch>
) : null} ) : null}
</> </>
); );

View File

@ -1,5 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FunctionComponent, useState } from 'react'; import React, { FunctionComponent, useState } from 'react';
import BGConfetti from '../../../assets/img/confetti-bg.jpeg';
import { Button } from '../../buttons/Button/Button'; import { Button } from '../../buttons/Button/Button';
type Props = { type Props = {
@ -15,48 +16,44 @@ const description = [
export const FirstTimeUserModal: FunctionComponent<Props> = ({ export const FirstTimeUserModal: FunctionComponent<Props> = ({
onCancel, onCancel,
onSave,
}: Props) => { }: Props) => {
const [active, setActive] = useState<number>(0); const [active, setActive] = useState<number>(0);
const [lastSlide, setLastSlide] = useState<boolean>(false); const [lastSlide, setLastSlide] = useState<boolean>(false);
const previousClick = () => { const previousClick = () => {
if (lastSlide) { setActive((pre) => pre - 1);
// to somthing setLastSlide(false);
} else {
setActive((pre) => pre - 1);
}
}; };
const nextClick = () => { const nextClick = () => {
if (lastSlide) { setActive((pre) => {
onCancel(); const newVal = pre + 1;
} else { setLastSlide(description.length - 1 === newVal);
setActive((pre) => {
const newVal = pre + 1;
description.length - 1 === newVal && setLastSlide(true);
return newVal; return newVal;
}); });
}
}; };
return ( return (
<dialog className="tw-modal"> <dialog className="tw-modal">
<div className="tw-modal-backdrop tw-opacity-80" /> <div className="tw-modal-backdrop tw-opacity-80" />
<div className="tw-modal-container tw-max-w-xl tw-max-h-90vh tw-bg-gradient-to-bl tw-to-primary-lite tw-from-secondary-lite"> <div
<div className="tw-modal-header tw-border-0 tw-justify-center tw-pt-8"> className="tw-modal-container tw-modal-confetti tw-max-w-xl tw-max-h-90vh"
<p className="tw-modal-title tw-text-h4 tw-font-semibold tw-text-primary-active"> style={{ backgroundImage: `url(${BGConfetti})` }}>
<div className="tw-modal-header tw-border-0 tw-justify-center tw-pt-8 tw-pb-0">
<p className="tw-modal-title tw-text-h4 tw-font-semibold tw-text-primary-active tw-mt-32">
Welcome to OpenMetadata Welcome to OpenMetadata
</p> </p>
</div> </div>
<div className="tw-modal-body tw-relative tw-h-64 tw-justify-center tw-items-center"> <div className="tw-modal-body tw-relative tw-h-40 tw-justify-start tw-items-center">
{description.map((d, i) => ( {description.map((d, i) => (
<p <p
className={classNames( className={classNames(
i === active i === active
? 'tw-opacity-100 tw-relative tw-transition-opacity tw-delay-200' ? 'tw-opacity-100 tw-relative tw-transition-opacity tw-delay-200'
: 'tw-opacity-0 tw-absolute', : 'tw-opacity-0 tw-absolute',
'tw-text-xl tw-font-medium tw-text-center' 'tw-text-xl tw-font-medium tw-text-center tw-bg-white tw-mx-7'
)} )}
key={i}> key={i}>
{d} {d}
@ -66,38 +63,48 @@ export const FirstTimeUserModal: FunctionComponent<Props> = ({
<div className="tw-modal-footer tw-border-0 tw-justify-between"> <div className="tw-modal-footer tw-border-0 tw-justify-between">
<Button <Button
className={classNames( className={classNames(
'tw-bg-primary-active tw-text-white', 'tw-text-primary-active',
active === 0 ? 'tw-invisible' : null active === 0 ? 'tw-invisible' : null
)} )}
size="regular" size="regular"
theme="primary" theme="primary"
variant="contained" variant="text"
onClick={previousClick}> onClick={previousClick}>
{lastSlide ? ( <i className="fas fa-arrow-left tw-text-sm tw-align-middle tw-pr-1.5" />{' '}
'Take a Tour' <span>Previous</span>
) : (
<>
<i className="fas fa-arrow-left tw-text-sm tw-align-middle tw-pr-1.5" />{' '}
<span>Previous</span>
</>
)}
</Button>
<Button
className="tw-bg-primary-active tw-text-white"
size="regular"
theme="primary"
variant="contained"
onClick={nextClick}>
{lastSlide ? (
'Skip and go to landing page'
) : (
<>
<span>Next</span>
<i className="fas fa-arrow-right tw-text-sm tw-align-middle tw-pl-1.5" />
</>
)}
</Button> </Button>
{lastSlide ? (
<span>
<Button
className="tw-text-primary-active tw-hidden"
size="regular"
theme="default"
variant="text"
onClick={onCancel}>
<span>Skip</span>
<i className="fas fa-angle-double-right tw-text-sm tw-align-middle tw-pl-1.5" />
</Button>
<Button
className="tw-bg-primary-active tw-text-white"
id="take-tour"
size="regular"
theme="primary"
variant="contained"
onClick={onSave}>
Explore OpenMetadata
</Button>
</span>
) : (
<Button
className="tw-text-primary-active"
size="regular"
theme="primary"
variant="text"
onClick={nextClick}>
<span>Next</span>
<i className="fas fa-arrow-right tw-text-sm tw-align-middle tw-pl-1.5" />
</Button>
)}
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@ -38,5 +38,6 @@ export const useAuth = (pathname = '') => {
isAuthenticatedRoute: isAuthenticatedRoute, isAuthenticatedRoute: isAuthenticatedRoute,
isAuthDisabled: authDisabled, isAuthDisabled: authDisabled,
isAdminUser: userDetails?.isAdmin, isAdminUser: userDetails?.isAdmin,
isFirstTimeUser: !isEmpty(userDetails) && !isEmpty(newUser),
}; };
}; };

View File

@ -40,7 +40,7 @@ const SigninPage = () => {
<PageContainer> <PageContainer>
<div className="tw-w-screen tw-h-screen tw-flex tw-justify-center"> <div className="tw-w-screen tw-h-screen tw-flex tw-justify-center">
<div className="tw-flex tw-flex-col tw-items-center signin-box"> <div className="tw-flex tw-flex-col tw-items-center signin-box">
<div className="tw-flex tw-justify-center tw-items-center tw-my-7"> <div className="tw-flex tw-justify-center tw-items-center tw-mb-7 tw-mt-20">
<SVGIcons <SVGIcons
alt="OpenMetadata Logo" alt="OpenMetadata Logo"
icon={Icons.LOGO_SMALL} icon={Icons.LOGO_SMALL}
@ -56,7 +56,7 @@ const SigninPage = () => {
<h6 className="tw-mb-px">Centralized Metadata Store, Discover,</h6> <h6 className="tw-mb-px">Centralized Metadata Store, Discover,</h6>
<h6 className="tw-mb-px">Collaborate and get your Data Right</h6> <h6 className="tw-mb-px">Collaborate and get your Data Right</h6>
</div> </div>
<div className="tw-mt-16" onClick={handleSignIn}> <div className="tw-mt-4" onClick={handleSignIn}>
{appState.authProvider.provider === AuthTypes.GOOGLE && ( {appState.authProvider.provider === AuthTypes.GOOGLE && (
<button className="tw-signin-button"> <button className="tw-signin-button">
<SVGIcons <SVGIcons

View File

@ -110,11 +110,17 @@
.tw-modal { .tw-modal {
@apply tw-z-9999 tw-flex tw-fixed tw-inset-0 tw-bg-transparent tw-justify-center tw-h-screen tw-w-screen tw-items-center tw-antialiased; @apply tw-z-9999 tw-flex tw-fixed tw-inset-0 tw-bg-transparent tw-justify-center tw-h-screen tw-w-screen tw-items-center tw-antialiased;
} }
.tw-modal-confetti {
background-size: cover;
background-position-y: -75px;
background-repeat: no-repeat;
background-position-x: center;
}
.tw-modal-backdrop { .tw-modal-backdrop {
@apply tw-opacity-60 tw-bg-body-hover tw-absolute tw-inset-0; @apply tw-opacity-60 tw-bg-body-hover tw-absolute tw-inset-0;
} }
.tw-modal-container { .tw-modal-container {
@apply tw-flex tw-flex-col tw-absolute tw-py-5 tw-bg-white tw-w-11/12 tw-max-w-screen-lg tw-max-h-screen tw-mx-auto tw-rounded-lg tw-border tw-border-main tw-shadow-xl; @apply tw-flex tw-flex-col tw-absolute tw-py-5 tw-bg-white tw-w-11/12 tw-max-w-screen-lg tw-max-h-screen tw-mx-auto tw-rounded-lg tw-border tw-border-main tw-shadow-modal;
} }
.tw-modal-header { .tw-modal-header {
@apply tw-flex tw-flex-row tw-justify-between tw-px-6 tw-pb-5 tw-bg-transparent tw-border-b tw-border-separator; @apply tw-flex tw-flex-row tw-justify-between tw-px-6 tw-pb-5 tw-bg-transparent tw-border-b tw-border-separator;
@ -280,13 +286,11 @@
} }
.signin-box { .signin-box {
@apply tw-m-auto tw-h-100 tw-w-120; @apply tw-m-auto tw-h-100 tw-w-120 tw-bg-white tw-shadow-modal;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
} }
.signup-box { .signup-box {
@apply tw-m-auto tw-w-120 tw-bg-white; @apply tw-m-auto tw-w-120 tw-bg-white tw-shadow-modal;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
} }
.disable-cta * { .disable-cta * {

View File

@ -11,11 +11,12 @@ export const getOidcExpiry = () => {
export const getUserManagerConfig = ( export const getUserManagerConfig = (
authClient: Record<string, string> = {} authClient: Record<string, string> = {}
): Record<string, string | WebStorageStateStore> => { ): Record<string, string | boolean | WebStorageStateStore> => {
const { authority, clientId, callbackUrl } = authClient; const { authority, clientId, callbackUrl } = authClient;
return { return {
authority, authority,
automaticSilentRenew: true,
// eslint-disable-next-line @typescript-eslint/camelcase // eslint-disable-next-line @typescript-eslint/camelcase
client_id: clientId, client_id: clientId,
// eslint-disable-next-line @typescript-eslint/camelcase // eslint-disable-next-line @typescript-eslint/camelcase

View File

@ -22,6 +22,8 @@ const primary = '#7147E8';
const primaryHover = '#5523E0'; const primaryHover = '#5523E0';
const primaryActive = '#450DE2'; const primaryActive = '#450DE2';
const primaryHoverLite = '#DBD1F9'; const primaryHoverLite = '#DBD1F9';
const secondary = '#B02AAC';
const secondaryBG = '#B02AAC40';
// state colors // state colors
const success = '#51C41A'; const success = '#51C41A';
@ -66,6 +68,9 @@ module.exports = {
focus: primary, focus: primary,
search: '#D5D6D9', search: '#D5D6D9',
}, },
boxShadow: {
modal: '1px 1px 5px rgba(0, 0, 0, 0.2)',
},
colors: { colors: {
'grey-body': textBody, 'grey-body': textBody,
'grey-muted': textMuted, 'grey-muted': textMuted,
@ -76,6 +81,8 @@ module.exports = {
'primary-hover': primaryHover, 'primary-hover': primaryHover,
'primary-active': primaryActive, 'primary-active': primaryActive,
'primary-hover-lite': primaryHoverLite, 'primary-hover-lite': primaryHoverLite,
secondary: secondary,
'secondary-lite': secondaryBG,
'body-main': bodyBG, 'body-main': bodyBG,
'body-hover': bodyHoverBG, 'body-hover': bodyHoverBG,
tag: tagBG, tag: tagBG,

View File

@ -80,7 +80,7 @@ module.exports = {
}, },
// //
{ {
test: /\.(png|jpg|gif|svg|ico)$/i, test: /\.(png|jpg|jpeg|gif|svg|ico)$/i,
use: [ use: [
{ {
loader: 'url-loader', loader: 'url-loader',

View File

@ -81,7 +81,7 @@ module.exports = {
}, },
// //
{ {
test: /\.(png|jpg|gif|svg|ico)$/i, test: /\.(png|jpg|jpeg|gif|svg|ico)$/i,
use: [ use: [
{ {
loader: 'url-loader', loader: 'url-loader',