change(ui): fix loading animation, add health modal to signup page (/cached in localstorage)

This commit is contained in:
nick-delirium 2023-03-23 13:52:08 +01:00 committed by Delirium
parent cbd4e4f669
commit b02bf8c23d
9 changed files with 172 additions and 69 deletions

View file

@ -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"

View file

@ -7,34 +7,41 @@ 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) {
if (!healthResponse?.overallHealth) {
if (healthResponse?.healthMap) {
setSelectedService(
Object.keys(healthResponse.healthMap).filter(
(s) => !healthResponse.healthMap[s].healthOk
)[0]
);
}
}
}, [healthResponse]);
const handleClose = () => {
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,7 +85,10 @@ function HealthModal({
<div className={'flex w-full'}>
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
{Object.keys(healthResponse.healthMap).map((service) => (
{isLoading ? (
<Category onClick={() => null} name={"Loading health status"} isLoading />
)
: Object.keys(healthResponse.healthMap).map((service) => (
<React.Fragment key={service}>
<Category
onClick={() => setSelectedService(service)}
@ -92,23 +102,40 @@ function HealthModal({
</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>
{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'}>
<Button
disabled={!healthResponse?.overallHealth}
loading={isLoading}
variant={'primary'}
className={'ml-auto'}
onClick={() => setPassed?.()}
>
Create Account
</Button>
</div>
<Footer />
) : null}
<Footer isSetup />
</div>
</div>
);

View file

@ -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}
</>
);

View file

@ -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}

View file

@ -4,3 +4,6 @@ export const categoryKeyNames = {
ingestionPipeline: 'Ingestion Pipeline',
ssl: 'SSL',
} as const
export const lastAskedKey = '__openreplay_health_status';
export const healthResponseKey = '__openreplay_health_response';

View 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 }
}

View file

@ -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

View 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