feat(ui) - getting started (#1148)
* feat(ui) - getting started - wip * feat(ui) - getting started - wip * feat(ui) - getting started - wip * feat(ui) - getting started - wip * change(ui) - getting started * change(ui) - getting started - css changes
This commit is contained in:
parent
578cb1af06
commit
08c5b11e30
11 changed files with 509 additions and 60 deletions
|
|
@ -20,6 +20,7 @@ import SettingsMenu from './SettingsMenu';
|
|||
import DefaultMenuView from './DefaultMenuView';
|
||||
import PreferencesView from './PreferencesView';
|
||||
import HealthStatus from './HealthStatus'
|
||||
import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress';
|
||||
|
||||
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
|
||||
|
||||
|
|
@ -60,11 +61,12 @@ const Header = (props) => {
|
|||
{!isPreferences && <DefaultMenuView siteId={siteId} />}
|
||||
{isPreferences && <PreferencesView />}
|
||||
<div className={styles.right}>
|
||||
{boardingCompletion < 75 && !hideDiscover && (
|
||||
{/* {boardingCompletion < 75 && !hideDiscover && (
|
||||
<React.Fragment>
|
||||
<OnboardingExplore onComplete={() => setHideDiscover(true)} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
)} */}
|
||||
<GettingStartedProgress />
|
||||
|
||||
<Notifications />
|
||||
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
percentage: number;
|
||||
radius?: number;
|
||||
progressColor?: string;
|
||||
bgColor?: string;
|
||||
label?: string;
|
||||
}
|
||||
const CircleProgress = ({
|
||||
percentage = 0,
|
||||
radius = 18,
|
||||
progressColor = '#394eff',
|
||||
bgColor = '#9fa8da',
|
||||
label = '',
|
||||
}: Props) => {
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const progress = percentage / 100;
|
||||
const _radius = radius * 0.8;
|
||||
const circumference = 2 * Math.PI * _radius;
|
||||
const offsetValue = circumference * (1 - progress);
|
||||
|
||||
setOffset(offsetValue);
|
||||
}, [percentage, radius]);
|
||||
|
||||
const strokeWidth = radius * 0.3;
|
||||
const _radius = radius * 0.8;
|
||||
|
||||
const circumference = 2 * Math.PI * _radius;
|
||||
const dashOffset = circumference * (1 - (percentage / 100));
|
||||
const circleStyle = {
|
||||
transition: 'stroke-dashoffset 1s ease-in-out',
|
||||
};
|
||||
|
||||
return (
|
||||
<svg width={radius * 2} height={radius * 2}>
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={_radius}
|
||||
stroke={bgColor}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={_radius}
|
||||
stroke={progressColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={dashOffset + ''}
|
||||
fill="none"
|
||||
transform={`rotate(-90 ${radius} ${radius})`}
|
||||
style={circleStyle}
|
||||
/>
|
||||
<text
|
||||
x={radius}
|
||||
y={radius}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={radius * 0.5}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircleProgress;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
|
||||
import GettingStartedModal, { Props } from './GettingStartedModal';
|
||||
import { Step } from './StepList';
|
||||
|
||||
const list: Step[] = [
|
||||
{
|
||||
title: '🕵️ Install OpenReplay',
|
||||
status: 'pending',
|
||||
description: 'Install OpenReplay on your website or mobile app.',
|
||||
icon: 'tools',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Identify Users',
|
||||
status: 'pending',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Integrations',
|
||||
status: 'completed',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Invite Team Members',
|
||||
status: 'ignored',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'GettingStarted',
|
||||
component: GettingStartedModal,
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<Props> = (args) => <GettingStartedModal {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
list,
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import StepList, { Step } from './StepList';
|
||||
import Modal from 'App/components/Modal/Modal';
|
||||
import CircleProgress from './CircleProgress';
|
||||
import GettingStartedProgress from './GettingStartedProgress';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
export interface Props {
|
||||
list: Step[];
|
||||
}
|
||||
|
||||
function GettingStartedModal(props: Props) {
|
||||
const { list } = props;
|
||||
const pendingSteps = list.filter((step) => step.status === 'pending');
|
||||
const completedSteps = list.filter(
|
||||
(step) => step.status === 'completed' || step.status === 'ignored'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header title="Setup Openreplay">
|
||||
<div className="px-4 pt-4">
|
||||
<div className="text-2xl">Setup Openreplay</div>
|
||||
<p>Find all the ways in which OpenReplay can benefit you and your product.</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content className="p-4 pb-20">
|
||||
<StepList title="Pending" steps={pendingSteps} status="pending" />
|
||||
<StepList title="Completed" steps={completedSteps} status="completed" />
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(GettingStartedModal);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import CircleProgress from './CircleProgress';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import GettingStartedModal from './GettingStartedModal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const GettingStartedProgress: React.FC<null> = () => {
|
||||
const { showModal } = useModal();
|
||||
|
||||
const {
|
||||
settingsStore: { gettingStarted },
|
||||
} = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
gettingStarted.fetchData();
|
||||
}, []);
|
||||
|
||||
const clickHandler = () => {
|
||||
showModal(<GettingStartedModal list={gettingStarted.steps} />, { right: true, width: 450 });
|
||||
};
|
||||
return gettingStarted.status === 'completed' ? null : (
|
||||
<div className="mr-6 flex items-cetner cursor-pointer hover:bg-active-blue px-4">
|
||||
<div className="flex items-center cursor-pointer" onClick={clickHandler}>
|
||||
<CircleProgress
|
||||
label={gettingStarted.label}
|
||||
percentage={gettingStarted.percentageCompleted}
|
||||
/>
|
||||
<div className="ml-2">
|
||||
<div className="text-lg color-teal" style={{ lineHeight: '15px' }}>
|
||||
Setup
|
||||
</div>
|
||||
<div className="color-gray-meidum text-sm">{gettingStarted.numPending} Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(GettingStartedProgress);
|
||||
103
frontend/app/components/shared/GettingStarted/StepList.tsx
Normal file
103
frontend/app/components/shared/GettingStarted/StepList.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { Step } from 'App/mstore/types/gettingStarted';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { onboarding as onboardingRoute, withSiteId } from 'App/routes';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
interface StepListProps extends RouteComponentProps {
|
||||
title: string;
|
||||
steps: Step[];
|
||||
status: 'pending' | 'completed';
|
||||
docsLink?: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
const StepItem = React.memo(
|
||||
({
|
||||
step,
|
||||
onClick,
|
||||
onIgnore,
|
||||
}: {
|
||||
step: Step;
|
||||
onIgnore: (e: React.MouseEvent<HTMLAnchorElement>, step: any) => void;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const { title, description, status, docsLink } = step;
|
||||
const isCompleted = status === 'completed';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('border rounded p-3 mb-4 flex items-start', {
|
||||
'bg-gray-lightest': isCompleted,
|
||||
'hover:bg-active-blue': !isCompleted,
|
||||
})}
|
||||
>
|
||||
<div className="w-10 mt-1 shrink-0">
|
||||
<Icon
|
||||
name={isCompleted ? 'check-circle-fill' : 'check-circle'}
|
||||
size={20}
|
||||
color={isCompleted ? 'teal' : 'gray-dark'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={cn('font-medium', { link: !isCompleted })} onClick={!isCompleted ? onClick : () => {}}>{title}</div>
|
||||
<div className="text-sm">{description}</div>
|
||||
<div className="flex gap-6 mt-3">
|
||||
<a className="link" href={docsLink} target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
{!isCompleted && (
|
||||
<a className="link" onClick={(e) => onIgnore(e, step)}>
|
||||
Ignore
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const StepList = React.memo((props: StepListProps) => {
|
||||
const { title, steps, status } = props;
|
||||
const { hideModal } = useModal();
|
||||
|
||||
const {
|
||||
settingsStore: { gettingStarted },
|
||||
} = useStore();
|
||||
|
||||
const onIgnore = (e: React.MouseEvent<HTMLAnchorElement>, step: any) => {
|
||||
e.preventDefault();
|
||||
gettingStarted.completeStep(step);
|
||||
};
|
||||
|
||||
if (steps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = (step: any) => {
|
||||
const { siteId, history } = props;
|
||||
console.log('step', withSiteId(onboardingRoute(step.url), siteId));
|
||||
hideModal();
|
||||
history.push(withSiteId(onboardingRoute(step.url), siteId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-3">
|
||||
<div className="text-lg font-medium mb-2">
|
||||
{title} {steps.length}
|
||||
</div>
|
||||
{steps.map((step) => (
|
||||
<StepItem key={step.title} onIgnore={onIgnore} step={step} onClick={() => onClick(step)}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default connect((state: any) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
}))(withRouter(StepList));
|
||||
1
frontend/app/components/shared/GettingStarted/index.ts
Normal file
1
frontend/app/components/shared/GettingStarted/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './GettingStartedModal';
|
||||
|
|
@ -4,4 +4,5 @@ export const DURATION_FILTER = "__$session-durationFilter$__"
|
|||
export const SESSION_FILTER = "__$session-filter$__"
|
||||
export const GLOBAL_DESTINATION_PATH = "__$global-destinationPath$__"
|
||||
export const GLOBAL_HAS_NO_RECORDINGS = "__$global-hasNoRecordings$__"
|
||||
export const SITE_ID_STORAGE_KEY = "__$user-siteId$__"
|
||||
export const SITE_ID_STORAGE_KEY = "__$user-siteId$__"
|
||||
export const GETTING_STARTED = "__$user-gettingStarted$__"
|
||||
|
|
@ -1,98 +1,102 @@
|
|||
import { makeAutoObservable, observable } from "mobx"
|
||||
import SessionSettings from "./types/sessionSettings"
|
||||
import { sessionService } from "App/services"
|
||||
import { makeAutoObservable, observable } from 'mobx';
|
||||
import SessionSettings from './types/sessionSettings';
|
||||
import { sessionService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import Webhook, { IWebhook } from 'Types/webhook';
|
||||
import {
|
||||
webhookService
|
||||
} from 'App/services';
|
||||
import { webhookService } from 'App/services';
|
||||
import { GettingStarted } from './types/gettingStarted';
|
||||
|
||||
export default class SettingsStore {
|
||||
loadingCaptureRate: boolean = false;
|
||||
sessionSettings: SessionSettings = new SessionSettings()
|
||||
sessionSettings: SessionSettings = new SessionSettings();
|
||||
captureRateFetched: boolean = false;
|
||||
limits: any = null;
|
||||
|
||||
webhooks: Webhook[] = []
|
||||
webhookInst = new Webhook()
|
||||
|
||||
hooksLoading = false
|
||||
webhooks: Webhook[] = [];
|
||||
webhookInst = new Webhook();
|
||||
hooksLoading = false;
|
||||
gettingStarted: GettingStarted = new GettingStarted();
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
sessionSettings: observable,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
saveCaptureRate(data: any) {
|
||||
return sessionService.saveCaptureRate(data)
|
||||
.then(data => data.json())
|
||||
return sessionService
|
||||
.saveCaptureRate(data)
|
||||
.then((data) => data.json())
|
||||
.then(({ data }) => {
|
||||
this.sessionSettings.merge({
|
||||
captureRate: data.rate,
|
||||
captureAll: data.captureAll
|
||||
})
|
||||
toast.success("Settings updated successfully");
|
||||
}).catch(err => {
|
||||
toast.error("Error saving capture rate");
|
||||
captureAll: data.captureAll,
|
||||
});
|
||||
toast.success('Settings updated successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Error saving capture rate');
|
||||
});
|
||||
}
|
||||
|
||||
fetchCaptureRate(): Promise<any> {
|
||||
this.loadingCaptureRate = true;
|
||||
return sessionService.fetchCaptureRate()
|
||||
.then(data => {
|
||||
return sessionService
|
||||
.fetchCaptureRate()
|
||||
.then((data) => {
|
||||
this.sessionSettings.merge({
|
||||
captureRate: data.rate,
|
||||
captureAll: data.captureAll
|
||||
})
|
||||
captureAll: data.captureAll,
|
||||
});
|
||||
this.captureRateFetched = true;
|
||||
}).finally(() => {
|
||||
this.loadingCaptureRate = false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingCaptureRate = false;
|
||||
});
|
||||
}
|
||||
|
||||
fetchWebhooks = () => {
|
||||
this.hooksLoading = true
|
||||
return webhookService.fetchList()
|
||||
.then(data => {
|
||||
this.webhooks = data.map(hook => new Webhook(hook))
|
||||
this.hooksLoading = false
|
||||
})
|
||||
}
|
||||
this.hooksLoading = true;
|
||||
return webhookService.fetchList().then((data) => {
|
||||
this.webhooks = data.map((hook) => new Webhook(hook));
|
||||
this.hooksLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
initWebhook = (inst?: Partial<IWebhook> | Webhook) => {
|
||||
this.webhookInst = inst instanceof Webhook ? inst : new Webhook(inst)
|
||||
}
|
||||
this.webhookInst = inst instanceof Webhook ? inst : new Webhook(inst);
|
||||
};
|
||||
|
||||
saveWebhook = (inst: Webhook) => {
|
||||
this.hooksLoading = true
|
||||
return webhookService.saveWebhook(inst)
|
||||
.then(data => {
|
||||
this.webhookInst = new Webhook(data)
|
||||
if (inst.webhookId === undefined) this.setWebhooks([...this.webhooks, this.webhookInst])
|
||||
else this.setWebhooks([...this.webhooks.filter(hook => hook.webhookId !== data.webhookId), this.webhookInst])
|
||||
|
||||
})
|
||||
.finally(() => {
|
||||
this.hooksLoading = false
|
||||
this.hooksLoading = true;
|
||||
return webhookService
|
||||
.saveWebhook(inst)
|
||||
.then((data) => {
|
||||
this.webhookInst = new Webhook(data);
|
||||
if (inst.webhookId === undefined) this.setWebhooks([...this.webhooks, this.webhookInst]);
|
||||
else
|
||||
this.setWebhooks([
|
||||
...this.webhooks.filter((hook) => hook.webhookId !== data.webhookId),
|
||||
this.webhookInst,
|
||||
]);
|
||||
})
|
||||
}
|
||||
.finally(() => {
|
||||
this.hooksLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
setWebhooks = (webhooks: Webhook[]) => {
|
||||
this.webhooks = webhooks
|
||||
}
|
||||
this.webhooks = webhooks;
|
||||
};
|
||||
|
||||
removeWebhook = (hookId: string) => {
|
||||
this.hooksLoading = true
|
||||
return webhookService.removeWebhook(hookId)
|
||||
.then(() => {
|
||||
this.webhooks = this.webhooks.filter(hook => hook.webhookId!== hookId)
|
||||
this.hooksLoading = false
|
||||
})
|
||||
}
|
||||
this.hooksLoading = true;
|
||||
return webhookService.removeWebhook(hookId).then(() => {
|
||||
this.webhooks = this.webhooks.filter((hook) => hook.webhookId !== hookId);
|
||||
this.hooksLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
editWebhook = (diff: Partial<IWebhook>) => {
|
||||
Object.assign(this.webhookInst, diff)
|
||||
}
|
||||
Object.assign(this.webhookInst, diff);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
141
frontend/app/mstore/types/gettingStarted.ts
Normal file
141
frontend/app/mstore/types/gettingStarted.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { configService } from 'App/services';
|
||||
import { GETTING_STARTED } from 'App/constants/storageKeys';
|
||||
|
||||
const stepsMap: any = {
|
||||
'Install OpenReplay': {
|
||||
title: '🛠️ Install OpenReplay',
|
||||
status: 'pending',
|
||||
description: 'Install via script or NPM package',
|
||||
docsLink: 'https://docs.openreplay.com/en/sdk/constructor/',
|
||||
url: 'installing',
|
||||
},
|
||||
'Identify Users': {
|
||||
title: '🕵️ Identify Users',
|
||||
status: 'pending',
|
||||
description: 'Filter sessions by user ID.',
|
||||
docsLink: 'https://docs.openreplay.com/en/v1.10.0/installation/identify-user/',
|
||||
url: 'identify-users',
|
||||
},
|
||||
'Invite Team Members': {
|
||||
title: '🧑💻 Invite Team Members',
|
||||
status: 'pending',
|
||||
description: 'Invite team members, collaborate and start improving your app now.',
|
||||
docsLink: 'https://docs.openreplay.com/en/tutorials/adding-users/',
|
||||
url: 'team',
|
||||
},
|
||||
Integrations: {
|
||||
title: '🔌 Integrations',
|
||||
status: 'pending',
|
||||
description: 'Sync your backend errors with sessions replays.',
|
||||
docsLink: 'https://docs.openreplay.com/en/integrations/',
|
||||
url: 'integrations',
|
||||
},
|
||||
};
|
||||
export interface Step {
|
||||
title: string;
|
||||
status: 'pending' | 'ignored' | 'completed';
|
||||
description: string;
|
||||
url: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
export class GettingStarted {
|
||||
steps: Step[] = [];
|
||||
status: 'in-progress' | 'completed';
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
steps: observable,
|
||||
completeStep: action,
|
||||
status: observable,
|
||||
fetchData: action,
|
||||
numCompleted: computed,
|
||||
numPending: computed,
|
||||
percentageCompleted: computed,
|
||||
label: computed,
|
||||
numPendingSteps: computed,
|
||||
});
|
||||
|
||||
// steps = {'tenatId': {steps: [], status: 'in-progress'}
|
||||
const gettingStartedSteps = localStorage.getItem(GETTING_STARTED);
|
||||
if (gettingStartedSteps) {
|
||||
const steps = JSON.parse(gettingStartedSteps);
|
||||
this.steps = steps.steps;
|
||||
this.status = steps.status;
|
||||
}
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
if (this.status === 'completed') {
|
||||
return;
|
||||
}
|
||||
configService.fetchGettingStarted().then((data) => {
|
||||
this.steps = data.map((item: any) => {
|
||||
const step = stepsMap[item.task];
|
||||
|
||||
return {
|
||||
...step,
|
||||
status: item.done ? 'completed' : 'pending',
|
||||
};
|
||||
});
|
||||
this.status = this.calculateStatus();
|
||||
this.updateLocalStorage();
|
||||
});
|
||||
}
|
||||
|
||||
updateLocalStorage() {
|
||||
localStorage.setItem(
|
||||
GETTING_STARTED,
|
||||
JSON.stringify({
|
||||
steps: this.steps.map((item: any) => ({
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
})),
|
||||
status: this.status,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
calculateStatus() {
|
||||
const numCompleted = this.numCompleted;
|
||||
const numPending = this.numPending;
|
||||
const numIgnored = this.steps.length - numCompleted - numPending;
|
||||
|
||||
if (numIgnored > 0) {
|
||||
return 'in-progress';
|
||||
} else {
|
||||
return numPending > 0 ? 'in-progress' : 'completed';
|
||||
}
|
||||
}
|
||||
|
||||
completeStep(step: Step) {
|
||||
step.status = 'completed';
|
||||
this.status = this.calculateStatus();
|
||||
this.updateLocalStorage();
|
||||
}
|
||||
|
||||
get numCompleted() {
|
||||
return this.steps.filter((step) => step.status === 'completed').length;
|
||||
}
|
||||
|
||||
get numPending() {
|
||||
return this.steps.filter((step) => step.status === 'pending').length;
|
||||
}
|
||||
|
||||
get percentageCompleted() {
|
||||
const completed = this.numCompleted;
|
||||
const total = this.steps.length;
|
||||
return Math.round((completed / total) * 100);
|
||||
}
|
||||
|
||||
get label() {
|
||||
const completed = this.numCompleted;
|
||||
const total = this.steps.length;
|
||||
return `${completed}/${total}`;
|
||||
}
|
||||
|
||||
get numPendingSteps() {
|
||||
return this.steps.filter((step) => step.status === 'pending').length;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,4 +14,9 @@ export default class ConfigService extends BaseService {
|
|||
return this.client.post('/config/weekly_report', config)
|
||||
.then(r => r.json()).then(j => j.data)
|
||||
}
|
||||
|
||||
async fetchGettingStarted(): Promise<any> {
|
||||
return this.client.get('/boarding')
|
||||
.then(r => r.json()).then(j => j.data)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue