[IMPROVE] Rewrite Federation Dashboard (#17900)
parent
ed7eca54c8
commit
ac987a1d91
@ -1,141 +0,0 @@ |
||||
.status { |
||||
flex: 0 0 auto; |
||||
|
||||
width: 6px; |
||||
height: 6px; |
||||
margin: 0 7px; |
||||
|
||||
border-radius: 50%; |
||||
} |
||||
|
||||
.status.stable { |
||||
background-color: #2de0a5; |
||||
} |
||||
|
||||
.status.unstable { |
||||
background-color: #ffd21f; |
||||
} |
||||
|
||||
.status.failing { |
||||
background-color: #f5455c; |
||||
} |
||||
|
||||
.frame { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.group { |
||||
display: flex; |
||||
flex-direction: row; |
||||
flex: 100%; |
||||
|
||||
max-width: 100%; |
||||
margin: 10px; |
||||
|
||||
border-width: 1px; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.group.left { |
||||
justify-content: flex-start; |
||||
} |
||||
|
||||
.group.wrap { |
||||
flex-wrap: wrap; |
||||
} |
||||
|
||||
.overview-column { |
||||
flex: 100%; |
||||
|
||||
min-height: 20px; |
||||
margin: 15px 0; |
||||
} |
||||
|
||||
.overview-column.small { |
||||
max-width: 20%; |
||||
} |
||||
|
||||
.group .overview-column:not(:last-child) { |
||||
border-right: 1px solid #e9e9e9; |
||||
} |
||||
|
||||
.group .overview-column:nth-child(5n) { |
||||
border-right: 0; |
||||
} |
||||
|
||||
.overview-pill { |
||||
display: flex; |
||||
|
||||
width: 100%; |
||||
padding: 0 10px; |
||||
|
||||
user-select: text; |
||||
text-align: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.overview-item { |
||||
width: 100%; |
||||
|
||||
user-select: text; |
||||
text-align: center; |
||||
} |
||||
|
||||
.overview-item > .title { |
||||
display: inline-block; |
||||
|
||||
margin-top: 8px; |
||||
|
||||
text-transform: uppercase; |
||||
|
||||
color: #9ea2a8; |
||||
|
||||
font-size: 0.875rem; |
||||
font-weight: 300; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
display: inline-block; |
||||
|
||||
width: 100%; |
||||
|
||||
text-transform: capitalize; |
||||
|
||||
color: #383838; |
||||
|
||||
font-size: 1.75rem; |
||||
font-weight: 400; |
||||
line-height: 1; |
||||
} |
||||
|
||||
@media screen and (max-width: 925px) { |
||||
.overview-item > .title { |
||||
font-size: 0.5rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 800px) { |
||||
.overview-item > .title { |
||||
font-size: 0.875rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1.75rem; |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 600px) { |
||||
.overview-item > .title { |
||||
font-size: 0.5rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1rem; |
||||
} |
||||
} |
@ -1,32 +0,0 @@ |
||||
<template name="dashboard"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Federation_Dashboard"}} |
||||
<div class="content"> |
||||
<div class="section"> |
||||
<div class="section-content"> |
||||
<div class="group border-component-color"> |
||||
{{#each federationOverviewData}} |
||||
<div class="overview-column"> |
||||
<div class="overview-item"> |
||||
<span class="value">{{value}}</span> |
||||
<span class="title">{{_ title}}</span> |
||||
</div> |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
<div class="group left wrap border-component-color"> |
||||
{{#each federationPeers}} |
||||
<div class="overview-column small"> |
||||
<div class="overview-pill"> |
||||
<span class="title">{{domain}}</span> |
||||
</div> |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</template> |
@ -1,85 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { hasRole } from '../../../authorization'; |
||||
import { registerAdminRoute, registerAdminSidebarItem } from '../../../../client/admin'; |
||||
|
||||
import './dashboard.html'; |
||||
import './dashboard.css'; |
||||
|
||||
// Template controller
|
||||
let templateInstance; // current template instance/context
|
||||
|
||||
// Methods
|
||||
const updateOverviewData = () => { |
||||
Meteor.call('federation:getOverviewData', (error, result) => { |
||||
if (error) { |
||||
return; |
||||
} |
||||
|
||||
const { data } = result; |
||||
|
||||
templateInstance.federationOverviewData.set(data); |
||||
}); |
||||
}; |
||||
|
||||
const updateServers = () => { |
||||
Meteor.call('federation:getServers', (error, result) => { |
||||
if (error) { |
||||
return; |
||||
} |
||||
|
||||
const { data } = result; |
||||
|
||||
templateInstance.federationPeers.set(data); |
||||
}); |
||||
}; |
||||
|
||||
const updateData = () => { |
||||
updateOverviewData(); |
||||
updateServers(); |
||||
}; |
||||
|
||||
Template.dashboard.helpers({ |
||||
federationOverviewData() { |
||||
return templateInstance.federationOverviewData.get(); |
||||
}, |
||||
federationPeers() { |
||||
return templateInstance.federationPeers.get(); |
||||
}, |
||||
}); |
||||
|
||||
// Events
|
||||
Template.dashboard.onCreated(function() { |
||||
templateInstance = Template.instance(); |
||||
|
||||
this.federationOverviewData = new ReactiveVar(); |
||||
this.federationPeers = new ReactiveVar(); |
||||
}); |
||||
|
||||
Template.dashboard.onRendered(() => { |
||||
Tracker.autorun(updateData); |
||||
|
||||
setInterval(updateData, 10000); |
||||
}); |
||||
|
||||
// Route setup
|
||||
|
||||
registerAdminRoute('/federation-dashboard', { |
||||
name: 'federation-dashboard', |
||||
action() { |
||||
BlazeLayout.render('main', { center: 'dashboard', old: true }); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminSidebarItem({ |
||||
icon: 'discover', |
||||
href: 'federation-dashboard', |
||||
i18nLabel: 'Federation Dashboard', |
||||
permissionGranted() { |
||||
return hasRole(Meteor.userId(), 'admin'); |
||||
}, |
||||
}); |
@ -1 +0,0 @@ |
||||
import './admin/dashboard'; |
@ -0,0 +1,23 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import OverviewSection from './OverviewSection'; |
||||
import ServersSection from './ServersSection'; |
||||
|
||||
function FederationDashboardPage() { |
||||
const t = useTranslation(); |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Federation_Dashboard')} /> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Box margin='x24'> |
||||
<OverviewSection /> |
||||
<ServersSection /> |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
|
||||
export default FederationDashboardPage; |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import FederationDashboardPage from './FederationDashboardPage'; |
||||
|
||||
export default { |
||||
title: 'admin/federationDashboard/FederationDashboardPage', |
||||
component: FederationDashboardPage, |
||||
}; |
||||
|
||||
export const Default = () => <FederationDashboardPage />; |
@ -0,0 +1,17 @@ |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useRole } from '../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../NotAuthorizedPage'; |
||||
import FederationDashboardPage from './FederationDashboardPage'; |
||||
|
||||
const FederationDashboardRoute: FC<{}> = () => { |
||||
const authorized = useRole('admin'); |
||||
|
||||
if (!authorized) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <FederationDashboardPage />; |
||||
}; |
||||
|
||||
export default FederationDashboardRoute; |
@ -0,0 +1,40 @@ |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import CounterSet from '../../components/data/CounterSet'; |
||||
import { usePolledMethodData, AsyncState } from '../../contexts/ServerContext'; |
||||
|
||||
function OverviewSection() { |
||||
const t = useTranslation(); |
||||
const [overviewData, overviewStatus] = usePolledMethodData('federation:getOverviewData', [], 10000); |
||||
|
||||
const eventCount = (overviewStatus === AsyncState.LOADING && <Skeleton variant='text' />) |
||||
|| (overviewStatus === AsyncState.ERROR && <Box color='danger'>Error</Box>) |
||||
|| overviewData?.data[0]?.value; |
||||
const userCount = (overviewStatus === AsyncState.LOADING && <Skeleton variant='text' />) |
||||
|| (overviewStatus === AsyncState.ERROR && <Box color='danger'>Error</Box>) |
||||
|| overviewData?.data[1]?.value; |
||||
const serverCount = (overviewStatus === AsyncState.LOADING && <Skeleton variant='text' />) |
||||
|| (overviewStatus === AsyncState.ERROR && <Box color='danger'>Error</Box>) |
||||
|| overviewData?.data[2]?.value; |
||||
|
||||
return <CounterSet |
||||
counters={[ |
||||
{ |
||||
count: eventCount, |
||||
description: t('Number_of_events'), |
||||
}, |
||||
{ |
||||
count: userCount, |
||||
description: t('Number_of_federated_users'), |
||||
}, |
||||
{ |
||||
count: serverCount, |
||||
description: t('Number_of_federated_servers'), |
||||
}, |
||||
]} |
||||
/>; |
||||
} |
||||
|
||||
export default OverviewSection; |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import OverviewSection from './OverviewSection'; |
||||
|
||||
export default { |
||||
title: 'admin/federationDashboard/OverviewSection', |
||||
component: OverviewSection, |
||||
}; |
||||
|
||||
export const Default = () => <OverviewSection />; |
@ -0,0 +1,26 @@ |
||||
import { Box, Throbber } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { usePolledMethodData, AsyncState } from '../../contexts/ServerContext'; |
||||
|
||||
function ServersSection() { |
||||
const [serversData, serversStatus] = usePolledMethodData('federation:getServers', [], 10000); |
||||
|
||||
if (serversStatus === AsyncState.LOADING) { |
||||
return <Throbber align='center' />; |
||||
} |
||||
|
||||
if (serversData?.data?.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
return <Box withRichContent> |
||||
<ul> |
||||
{serversData?.data?.map(({ domain }) => ( |
||||
<li key={domain}>{domain}</li> |
||||
))} |
||||
</ul> |
||||
</Box>; |
||||
} |
||||
|
||||
export default ServersSection; |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import ServersSection from './ServersSection'; |
||||
|
||||
export default { |
||||
title: 'admin/federationDashboard/ServersSection', |
||||
component: ServersSection, |
||||
}; |
||||
|
||||
export const Default = () => <ServersSection />; |
@ -0,0 +1,20 @@ |
||||
import React from 'react'; |
||||
|
||||
import Counter from './Counter'; |
||||
|
||||
export default { |
||||
title: 'components/data/Counter', |
||||
component: Counter, |
||||
}; |
||||
|
||||
export const Default = () => |
||||
<Counter count={123} />; |
||||
|
||||
export const WithPositiveVariation = () => |
||||
<Counter count={123} variation={4} />; |
||||
|
||||
export const WithNegativeVariation = () => |
||||
<Counter count={123} variation={-4} />; |
||||
|
||||
export const WithDescription = () => |
||||
<Counter count={123} description='Description' />; |
@ -1,9 +1,9 @@ |
||||
import React from 'react'; |
||||
|
||||
import { CounterSet } from './CounterSet'; |
||||
import CounterSet from './CounterSet'; |
||||
|
||||
export default { |
||||
title: 'admin/enterprise/engagement/data/CounterSet', |
||||
title: 'components/data/CounterSet', |
||||
component: CounterSet, |
||||
}; |
||||
|
@ -0,0 +1,26 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import Growth from './Growth'; |
||||
|
||||
export default { |
||||
title: 'components/data/Growth', |
||||
component: Growth, |
||||
decorators: [(fn) => <Box children={fn()} margin='x16' />], |
||||
}; |
||||
|
||||
export const Positive = () => |
||||
<Growth>{3}</Growth>; |
||||
|
||||
export const Zero = () => |
||||
<Growth>{0}</Growth>; |
||||
|
||||
export const Negative = () => |
||||
<Growth>{-3}</Growth>; |
||||
|
||||
export const WithTextStyle = () => |
||||
['h1', 's1', 'c1', 'micro'] |
||||
.map((fontScale) => <Box key={fontScale}> |
||||
<Growth fontScale={fontScale}>{3}</Growth> |
||||
<Growth fontScale={fontScale}>{-3}</Growth> |
||||
</Box>); |
@ -0,0 +1,22 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import NegativeGrowthSymbol from './NegativeGrowthSymbol'; |
||||
|
||||
export default { |
||||
title: 'components/data/NegativeGrowthSymbol', |
||||
component: NegativeGrowthSymbol, |
||||
decorators: [ |
||||
(fn) => <Box children={fn()} margin='x16' />, |
||||
], |
||||
}; |
||||
|
||||
export const Default = () => <NegativeGrowthSymbol />; |
||||
|
||||
export const WithColor = () => <NegativeGrowthSymbol />; |
||||
|
||||
WithColor.story = { |
||||
decorators: [ |
||||
(storyFn) => <Box color='danger'>{storyFn()}</Box>, |
||||
], |
||||
}; |
@ -0,0 +1,24 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import PositiveGrowthSymbol from './PositiveGrowthSymbol'; |
||||
|
||||
export default { |
||||
title: 'components/data/PositiveGrowthSymbol', |
||||
component: PositiveGrowthSymbol, |
||||
decorators: [ |
||||
(fn) => <Box children={fn()} margin='x16' />, |
||||
], |
||||
}; |
||||
|
||||
export const Default = () => |
||||
<PositiveGrowthSymbol />; |
||||
|
||||
export const WithColor = () => |
||||
<PositiveGrowthSymbol />; |
||||
|
||||
WithColor.story = { |
||||
decorators: [ |
||||
(storyFn) => <Box color='success'>{storyFn()}</Box>, |
||||
], |
||||
}; |
@ -1,28 +0,0 @@ |
||||
import { createContext, useCallback, useContext } from 'react'; |
||||
|
||||
export const ServerContext = createContext({ |
||||
info: {}, |
||||
absoluteUrl: (path) => path, |
||||
callMethod: async () => {}, |
||||
callEndpoint: async () => {}, |
||||
upload: async () => {}, |
||||
}); |
||||
|
||||
export const useServerInformation = () => useContext(ServerContext).info; |
||||
|
||||
export const useAbsoluteUrl = () => useContext(ServerContext).absoluteUrl; |
||||
|
||||
export const useMethod = (methodName) => { |
||||
const { callMethod } = useContext(ServerContext); |
||||
return useCallback((...args) => callMethod(methodName, ...args), [callMethod, methodName]); |
||||
}; |
||||
|
||||
export const useEndpoint = (httpMethod, endpoint) => { |
||||
const { callEndpoint } = useContext(ServerContext); |
||||
return useCallback((...args) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]); |
||||
}; |
||||
|
||||
export const useUpload = (endpoint) => { |
||||
const { upload } = useContext(ServerContext); |
||||
return useCallback((...args) => upload(endpoint, ...args), [upload]); |
||||
}; |
@ -0,0 +1,111 @@ |
||||
import { createContext, useCallback, useContext, useMemo, useState, useEffect, useRef } from 'react'; |
||||
|
||||
interface IServerStream { |
||||
on(eventName: string, callback: (data: any) => void): void; |
||||
off(eventName: string, callback: (data: any) => void): void; |
||||
} |
||||
|
||||
type ServerContextValue = { |
||||
info: object; |
||||
absoluteUrl: (path: string) => string; |
||||
callMethod: (methodName: string, ...args: any[]) => Promise<any>; |
||||
callEndpoint: (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string, ...args: any[]) => Promise<any>; |
||||
uploadToEndpoint: (endpoint: string) => Promise<void>; |
||||
getStream: (streamName: string, options?: object) => IServerStream; |
||||
}; |
||||
|
||||
export const ServerContext = createContext<ServerContextValue>({ |
||||
info: {}, |
||||
absoluteUrl: (path) => path, |
||||
callMethod: async () => undefined, |
||||
callEndpoint: async () => undefined, |
||||
uploadToEndpoint: async () => undefined, |
||||
getStream: () => ({ |
||||
on: (): void => undefined, |
||||
off: (): void => undefined, |
||||
}), |
||||
}); |
||||
|
||||
export const useServerInformation = (): object => useContext(ServerContext).info; |
||||
|
||||
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl; |
||||
|
||||
export const useMethod = (methodName: string): (...args: any[]) => Promise<any> => { |
||||
const { callMethod } = useContext(ServerContext); |
||||
return useCallback((...args) => callMethod(methodName, ...args), [callMethod, methodName]); |
||||
}; |
||||
|
||||
export const useEndpoint = (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string): (...args: any[]) => Promise<any> => { |
||||
const { callEndpoint } = useContext(ServerContext); |
||||
return useCallback((...args) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]); |
||||
}; |
||||
|
||||
export const useUpload = (endpoint: string): () => Promise<void> => { |
||||
const { uploadToEndpoint } = useContext(ServerContext); |
||||
return useCallback((...args) => uploadToEndpoint(endpoint, ...args), [endpoint, uploadToEndpoint]); |
||||
}; |
||||
|
||||
export const useStream = (streamName: string, options?: object): IServerStream => { |
||||
const { getStream } = useContext(ServerContext); |
||||
return useMemo(() => getStream(streamName, options), [streamName, options]); |
||||
}; |
||||
|
||||
export enum AsyncState { |
||||
LOADING = 'loading', |
||||
DONE = 'done', |
||||
ERROR = 'error', |
||||
} |
||||
|
||||
export const useMethodData = <T>(methodName: string, args: any[] = []): [T | null, AsyncState, () => void] => { |
||||
const getData = useMethod(methodName); |
||||
const [[data, state], updateState] = useState<[T | null, AsyncState]>([null, AsyncState.LOADING]); |
||||
|
||||
const isMountedRef = useRef(true); |
||||
|
||||
useEffect(() => (): void => { |
||||
isMountedRef.current = false; |
||||
}, []); |
||||
|
||||
const fetchData = useCallback(() => { |
||||
updateState(([data]) => [data, AsyncState.LOADING]); |
||||
|
||||
getData(...args) |
||||
.then((data) => { |
||||
if (!isMountedRef.current) { |
||||
return; |
||||
} |
||||
|
||||
updateState([data, AsyncState.DONE]); |
||||
}) |
||||
.catch((error) => { |
||||
if (!isMountedRef.current) { |
||||
return; |
||||
} |
||||
|
||||
updateState(([data]) => [data, AsyncState.ERROR]); |
||||
console.error(error); |
||||
}); |
||||
}, [getData, ...args]); |
||||
|
||||
useEffect(() => { |
||||
fetchData(); |
||||
}, [fetchData]); |
||||
|
||||
return [data, state, fetchData]; |
||||
}; |
||||
|
||||
export const usePolledMethodData = <T>(methodName: string, args: any[] = [], intervalMs: number): [T | null, AsyncState, () => void] => { |
||||
const [data, state, fetchData] = useMethodData<T>(methodName, args); |
||||
|
||||
useEffect(() => { |
||||
const timer = setInterval(() => { |
||||
fetchData(); |
||||
}, intervalMs); |
||||
|
||||
return (): void => { |
||||
clearInterval(timer); |
||||
}; |
||||
}, []); |
||||
|
||||
return [data, state, fetchData]; |
||||
}; |
@ -1,16 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Counter } from './Counter'; |
||||
|
||||
export default { |
||||
title: 'admin/enterprise/engagement/data/Counter', |
||||
component: Counter, |
||||
}; |
||||
|
||||
export const _default = () => <Counter count={123} />; |
||||
|
||||
export const withPositiveVariation = () => <Counter count={123} variation={4} />; |
||||
|
||||
export const withNegativeVariation = () => <Counter count={123} variation={-4} />; |
||||
|
||||
export const withDescription = () => <Counter count={123} description='Description' />; |
@ -1,23 +0,0 @@ |
||||
import { Box, Margins } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { Growth } from './Growth'; |
||||
|
||||
export default { |
||||
title: 'admin/enterprise/engagement/data/Growth', |
||||
component: Growth, |
||||
decorators: [(fn) => <Margins children={fn()} all='x16' />], |
||||
}; |
||||
|
||||
export const positive = () => <Growth>{3}</Growth>; |
||||
|
||||
export const zero = () => <Growth>{0}</Growth>; |
||||
|
||||
export const negative = () => <Growth>{-3}</Growth>; |
||||
|
||||
export const withTextStyle = () => |
||||
['h1', 's1', 'c1', 'micro'] |
||||
.map((fontScale) => <Box key={fontScale}> |
||||
<Growth fontScale={fontScale}>{3}</Growth> |
||||
<Growth fontScale={fontScale}>{-3}</Growth> |
||||
</Box>); |
@ -1,16 +0,0 @@ |
||||
import { Box, Margins } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { NegativeGrowthSymbol } from './NegativeGrowthSymbol'; |
||||
|
||||
export default { |
||||
title: 'admin/enterprise/engagement/data/NegativeGrowthSymbol', |
||||
component: NegativeGrowthSymbol, |
||||
decorators: [(fn) => <Margins children={fn()} all='x16' />], |
||||
}; |
||||
|
||||
export const _default = () => <NegativeGrowthSymbol />; |
||||
|
||||
export const withColor = () => <Box color='danger'> |
||||
<NegativeGrowthSymbol /> |
||||
</Box>; |
@ -1,16 +0,0 @@ |
||||
import { Box, Margins } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { PositiveGrowthSymbol } from './PositiveGrowthSymbol'; |
||||
|
||||
export default { |
||||
title: 'admin/enterprise/engagement/data/PositiveGrowthSymbol', |
||||
component: PositiveGrowthSymbol, |
||||
decorators: [(fn) => <Margins children={fn()} all='x16' />], |
||||
}; |
||||
|
||||
export const _default = () => <PositiveGrowthSymbol />; |
||||
|
||||
export const withColor = () => <Box color='success'> |
||||
<PositiveGrowthSymbol /> |
||||
</Box>; |
Loading…
Reference in new issue