change(ui): fix loading animation, add health modal to signup page (/cached in localstorage)
This commit is contained in:
parent
cbd4e4f669
commit
b02bf8c23d
9 changed files with 172 additions and 69 deletions
|
|
@ -1,9 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames'
|
||||
|
||||
function Footer() {
|
||||
function Footer({ isSetup }: { isSetup?: boolean }) {
|
||||
return (
|
||||
<div className={'flex w-full p-4 items-center justify-center bg-gray-lightest gap-4'}>
|
||||
<div className={cn(
|
||||
'flex w-full p-4 items-center justify-center',
|
||||
'bg-gray-lightest gap-4',
|
||||
!isSetup ? 'border-t border-figmaColors-divider' : ''
|
||||
)}>
|
||||
<a
|
||||
href={'https://docs.openreplay.com/en/troubleshooting/'}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -7,27 +7,32 @@ import { getHighest } from 'App/constants/zindex';
|
|||
import Category from 'Components/Header/HealthStatus/ServiceCategory';
|
||||
import SubserviceHealth from 'Components/Header/HealthStatus/SubserviceHealth/SubserviceHealth';
|
||||
import { IServiceStats } from '../HealthStatus';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
function HealthModal({
|
||||
getHealth,
|
||||
isLoading,
|
||||
healthResponse,
|
||||
setShowModal,
|
||||
setPassed,
|
||||
}: {
|
||||
getHealth: () => void;
|
||||
isLoading: boolean;
|
||||
healthResponse: { overallHealth: boolean; healthMap: Record<string, IServiceStats> };
|
||||
setShowModal: (isOpen: boolean) => void;
|
||||
setPassed?: () => void;
|
||||
}) {
|
||||
const [selectedService, setSelectedService] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!healthResponse.overallHealth) {
|
||||
setSelectedService(
|
||||
Object.keys(healthResponse.healthMap).filter(
|
||||
(s) => !healthResponse.healthMap[s].healthOk
|
||||
)[0]
|
||||
);
|
||||
if (!healthResponse?.overallHealth) {
|
||||
if (healthResponse?.healthMap) {
|
||||
setSelectedService(
|
||||
Object.keys(healthResponse.healthMap).filter(
|
||||
(s) => !healthResponse.healthMap[s].healthOk
|
||||
)[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [healthResponse]);
|
||||
|
||||
|
|
@ -35,6 +40,8 @@ function HealthModal({
|
|||
setShowModal(false);
|
||||
};
|
||||
|
||||
const isSetup = document.location.pathname.includes('/signup')
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -54,7 +61,7 @@ function HealthModal({
|
|||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
height: '600px',
|
||||
height: isSetup ? '600px' : '535px',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
@ -78,37 +85,57 @@ function HealthModal({
|
|||
|
||||
<div className={'flex w-full'}>
|
||||
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
|
||||
{Object.keys(healthResponse.healthMap).map((service) => (
|
||||
<React.Fragment key={service}>
|
||||
<Category
|
||||
onClick={() => setSelectedService(service)}
|
||||
healthOk={healthResponse.healthMap[service].healthOk}
|
||||
name={healthResponse.healthMap[service].name}
|
||||
isSelectable
|
||||
isSelected={selectedService === service}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<Category onClick={() => null} name={"Loading health status"} isLoading />
|
||||
)
|
||||
: Object.keys(healthResponse.healthMap).map((service) => (
|
||||
<React.Fragment key={service}>
|
||||
<Category
|
||||
onClick={() => setSelectedService(service)}
|
||||
healthOk={healthResponse.healthMap[service].healthOk}
|
||||
name={healthResponse.healthMap[service].name}
|
||||
isSelectable
|
||||
isSelected={selectedService === service}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
'bg-gray-lightest border-l w-fit border-figmaColors-divider overflow-y-scroll'
|
||||
'bg-gray-lightest border-l w-fit border-figmaColors-divider overflow-y-scroll relative'
|
||||
}
|
||||
style={{ flex: 2, height: 420 }}
|
||||
>
|
||||
{selectedService ? (
|
||||
{isLoading ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(50% - 28px)',
|
||||
left: 'calc(50% - 28px)',
|
||||
}}
|
||||
>
|
||||
<AnimatedSVG name={ICONS.LOADER} size={56} />
|
||||
</div>
|
||||
) : selectedService ? (
|
||||
<ServiceStatus service={healthResponse.healthMap[selectedService]} />
|
||||
) : (
|
||||
<img src={slide} width={392} />
|
||||
)}
|
||||
) : <img src={slide} width={392} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'p-4 mt-auto w-full border-t border-figmaColors-divider'}>
|
||||
<Button disabled={!healthResponse.overallHealth} loading={isLoading} variant={'primary'} className={'ml-auto'}>
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
<Footer />
|
||||
{isSetup ? (
|
||||
<div className={'p-4 mt-auto w-full border-t border-figmaColors-divider'}>
|
||||
<Button
|
||||
disabled={!healthResponse?.overallHealth}
|
||||
loading={isLoading}
|
||||
variant={'primary'}
|
||||
className={'ml-auto'}
|
||||
onClick={() => setPassed?.()}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<Footer isSetup />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
|
||||
import { healthService } from 'App/services';
|
||||
import { categoryKeyNames } from './const';
|
||||
import { lastAskedKey, healthResponseKey } from './const';
|
||||
import HealthWidget from "Components/Header/HealthStatus/HealthWidget";
|
||||
import { getHealthRequest } from './getHealth'
|
||||
|
||||
export interface IServiceStats {
|
||||
name: 'backendServices' | 'databases' | 'ingestionPipeline' | 'ssl';
|
||||
|
|
@ -18,32 +18,8 @@ export interface IServiceStats {
|
|||
}[]
|
||||
}
|
||||
|
||||
function mapResponse(resp: Record<string, any>) {
|
||||
const services = Object.keys(resp);
|
||||
const healthMap: Record<string, IServiceStats> = {};
|
||||
services.forEach((service) => {
|
||||
healthMap[service] = {
|
||||
// @ts-ignore
|
||||
name: categoryKeyNames[service],
|
||||
healthOk: true,
|
||||
subservices: resp[service],
|
||||
serviceName: service,
|
||||
};
|
||||
Object.values(healthMap[service].subservices).forEach((subservice: Record<string, any>) => {
|
||||
if (!subservice?.health) healthMap[service].healthOk = false;
|
||||
});
|
||||
});
|
||||
|
||||
const overallHealth = Object.values(healthMap).every(
|
||||
(service: Record<string, any>) => service.healthOk
|
||||
);
|
||||
|
||||
return { overallHealth, healthMap };
|
||||
}
|
||||
|
||||
function HealthStatus() {
|
||||
const lastAskedKey = '__openreplay_health_status';
|
||||
const healthResponseKey = '__openreplay_health_response';
|
||||
const healthResponseSaved = localStorage.getItem(healthResponseKey) || '{}';
|
||||
const [healthResponse, setHealthResponse] = React.useState(JSON.parse(healthResponseSaved));
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
|
@ -55,12 +31,8 @@ function HealthStatus() {
|
|||
if (isLoading) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const r = await healthService.fetchStatus();
|
||||
const healthMap = mapResponse(r);
|
||||
const { healthMap, asked } = await getHealthRequest();
|
||||
setHealthResponse(healthMap);
|
||||
const asked = new Date().getTime();
|
||||
localStorage.setItem(healthResponseKey, JSON.stringify(healthMap));
|
||||
localStorage.setItem(lastAskedKey, asked.toString());
|
||||
setLastAsked(asked.toString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -82,10 +54,10 @@ function HealthStatus() {
|
|||
const icon = healthResponse?.overallHealth ? 'pulse' : ('exclamation-circle-fill' as const);
|
||||
return (
|
||||
<>
|
||||
<div className={'relative group h-full'}>
|
||||
<div className={'relative group h-full hover:bg-figmaColors-secondary-outlined-hover-background'}>
|
||||
<div
|
||||
className={
|
||||
'rounded cursor-pointer p-2 flex items-center hover:bg-figmaColors-secondary-outlined-hover-background'
|
||||
'rounded cursor-pointer p-2 flex items-center'
|
||||
}
|
||||
>
|
||||
<div className={'rounded p-2 border border-light-gray bg-white flex items-center '}>
|
||||
|
|
@ -102,7 +74,12 @@ function HealthStatus() {
|
|||
/>
|
||||
</div>
|
||||
{showModal ? (
|
||||
<HealthModal setShowModal={setShowModal} healthResponse={healthResponse} getHealth={getHealth} isLoading={isLoading} />
|
||||
<HealthModal
|
||||
setShowModal={setShowModal}
|
||||
healthResponse={healthResponse}
|
||||
getHealth={getHealth}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from 'UI';
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
function Category({
|
||||
name,
|
||||
|
|
@ -10,15 +11,18 @@ function Category({
|
|||
isExpandable,
|
||||
isExpanded,
|
||||
isSelected,
|
||||
isLoading,
|
||||
}: {
|
||||
name: string;
|
||||
healthOk: boolean;
|
||||
healthOk?: boolean;
|
||||
isLoading?: boolean;
|
||||
onClick: (args: any) => void;
|
||||
isSelectable?: boolean;
|
||||
isExpandable?: boolean;
|
||||
isExpanded?: boolean;
|
||||
isSelected?: boolean;
|
||||
}) {
|
||||
|
||||
const icon = healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const);
|
||||
return (
|
||||
<div
|
||||
|
|
@ -29,7 +33,9 @@ function Category({
|
|||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={icon} size={20} color={'green'} />
|
||||
{isLoading ? (
|
||||
<AnimatedSVG name={ICONS.LOADER} size={20} />
|
||||
) : <Icon name={icon} size={20} color={'green'} />}
|
||||
{name}
|
||||
|
||||
{isSelectable ? <Icon name={'chevron-right'} size={16} className={'ml-auto'} /> : null}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,7 @@ export const categoryKeyNames = {
|
|||
databases: 'Databases',
|
||||
ingestionPipeline: 'Ingestion Pipeline',
|
||||
ssl: 'SSL',
|
||||
} as const
|
||||
} as const
|
||||
|
||||
export const lastAskedKey = '__openreplay_health_status';
|
||||
export const healthResponseKey = '__openreplay_health_response';
|
||||
36
frontend/app/components/Header/HealthStatus/getHealth.ts
Normal file
36
frontend/app/components/Header/HealthStatus/getHealth.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { healthService } from 'App/services';
|
||||
import { categoryKeyNames, lastAskedKey, healthResponseKey } from "Components/Header/HealthStatus/const";
|
||||
import { IServiceStats } from "Components/Header/HealthStatus/HealthStatus";
|
||||
|
||||
|
||||
function mapResponse(resp: Record<string, any>) {
|
||||
const services = Object.keys(resp);
|
||||
const healthMap: Record<string, IServiceStats> = {};
|
||||
services.forEach((service) => {
|
||||
healthMap[service] = {
|
||||
// @ts-ignore
|
||||
name: categoryKeyNames[service],
|
||||
healthOk: true,
|
||||
subservices: resp[service],
|
||||
serviceName: service,
|
||||
};
|
||||
Object.values(healthMap[service].subservices).forEach((subservice: Record<string, any>) => {
|
||||
if (!subservice?.health) healthMap[service].healthOk = false;
|
||||
});
|
||||
});
|
||||
|
||||
const overallHealth = Object.values(healthMap).every(
|
||||
(service: Record<string, any>) => service.healthOk
|
||||
);
|
||||
|
||||
return { overallHealth, healthMap };
|
||||
}
|
||||
|
||||
export async function getHealthRequest() {
|
||||
const r = await healthService.fetchStatus();
|
||||
const healthMap = mapResponse(r);
|
||||
const asked = new Date().getTime();
|
||||
localStorage.setItem(healthResponseKey, JSON.stringify(healthMap));
|
||||
localStorage.setItem(lastAskedKey, asked.toString());
|
||||
return { healthMap, asked }
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import stl from './signup.module.css';
|
|||
import cn from 'classnames';
|
||||
import SignupForm from './SignupForm';
|
||||
import RegisterBg from '../../svg/register.svg';
|
||||
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
|
||||
import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
|
||||
|
||||
const BulletItem = ({ text }) => (
|
||||
<div className="flex items-center mb-4">
|
||||
|
|
@ -15,9 +17,45 @@ const BulletItem = ({ text }) => (
|
|||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const healthStatusCheck_key = '__or__healthStatusCheck_key'
|
||||
|
||||
@withPageTitle('Signup - OpenReplay')
|
||||
export default class Signup extends React.Component {
|
||||
state = {
|
||||
healthModalPassed: localStorage.getItem(healthStatusCheck_key === 'true'),
|
||||
healthStatusLoading: true,
|
||||
healthStatus: null,
|
||||
}
|
||||
|
||||
getHealth = async () => {
|
||||
this.setState({ healthStatusLoading: true });
|
||||
const { healthMap } = await getHealthRequest();
|
||||
this.setState({ healthStatus: healthMap, healthStatusLoading: false });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.state.healthModalPassed) void this.getHealth();
|
||||
}
|
||||
|
||||
setHealthModalPassed = () => {
|
||||
localStorage.setItem(healthStatusCheck_key, 'true');
|
||||
this.setState({ healthModalPassed: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.healthModalPassed) {
|
||||
return (
|
||||
<HealthModal
|
||||
setShowModal={() => null}
|
||||
healthResponse={this.state.healthStatus}
|
||||
getHealth={this.getHealth}
|
||||
isLoading={this.state.healthStatusLoading}
|
||||
setPassed={this.setHealthModalPassed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex" style={{ height: '100vh' }}>
|
||||
<div className={cn('w-6/12 relative overflow-hidden', stl.left)}>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
10
frontend/app/svg/icons/book-doc.svg
Normal file
10
frontend/app/svg/icons/book-doc.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2633_13173)">
|
||||
<path d="M1.5 3.328C2.385 2.958 3.654 2.559 4.888 2.435C6.218 2.301 7.346 2.498 8 3.187V12.933C7.065 12.403 5.88 12.33 4.787 12.44C3.607 12.56 2.417 12.901 1.5 13.251V3.328ZM9 3.187C9.654 2.498 10.782 2.301 12.112 2.435C13.346 2.559 14.615 2.958 15.5 3.328V13.251C14.582 12.901 13.393 12.559 12.213 12.441C11.119 12.33 9.935 12.402 9 12.933V3.187ZM8.5 2.283C7.515 1.436 6.087 1.31 4.787 1.44C3.273 1.593 1.745 2.112 0.793 2.545C0.705649 2.58473 0.631575 2.64875 0.579621 2.72943C0.527667 2.81011 0.500027 2.90404 0.5 3V14C0.500023 14.0837 0.521037 14.166 0.561117 14.2394C0.601197 14.3128 0.659062 14.375 0.729411 14.4203C0.79976 14.4656 0.880345 14.4925 0.963783 14.4985C1.04722 14.5046 1.13085 14.4896 1.207 14.455C2.089 14.055 3.51 13.574 4.887 13.435C6.296 13.293 7.477 13.522 8.11 14.312C8.15685 14.3704 8.21622 14.4175 8.28372 14.4499C8.35122 14.4823 8.42513 14.4991 8.5 14.4991C8.57487 14.4991 8.64878 14.4823 8.71628 14.4499C8.78378 14.4175 8.84315 14.3704 8.89 14.312C9.523 13.522 10.704 13.293 12.112 13.435C13.49 13.574 14.912 14.055 15.793 14.455C15.8692 14.4896 15.9528 14.5046 16.0362 14.4985C16.1197 14.4925 16.2002 14.4656 16.2706 14.4203C16.3409 14.375 16.3988 14.3128 16.4389 14.2394C16.479 14.166 16.5 14.0837 16.5 14V3C16.5 2.90404 16.4723 2.81011 16.4204 2.72943C16.3684 2.64875 16.2944 2.58473 16.207 2.545C15.255 2.112 13.727 1.593 12.213 1.44C10.913 1.309 9.485 1.436 8.5 2.283Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2633_13173">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
Loading…
Add table
Reference in a new issue