refactor(app/widgets): create widgets react components [EE-1813] (#6097)

pull/6028/head
Chaim Lev-Ari 2021-11-16 16:51:49 +02:00 committed by GitHub
parent 1a6af5d58f
commit bcaf20caca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 323 additions and 94 deletions

View File

@ -307,7 +307,6 @@ a[ng-click] {
.custom-header-ico {
max-width: 32px;
max-height: 32px;
margin-right: 2px;
}
.btn-responsive {

View File

@ -4,11 +4,12 @@ import sidebarModule from './sidebar';
import gitFormModule from './forms/git-form';
import porAccessManagementModule from './accessManagement';
import formComponentsModule from './form-components';
import widgetModule from './widget';
import { ReactExampleAngular } from './ReactExample';
import { TooltipAngular } from './Tooltip';
export default angular
.module('portainer.app.components', [sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
.module('portainer.app.components', [widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
.component('portainerTooltip', TooltipAngular)
.component('reactExample', ReactExampleAngular).name;

View File

@ -1,7 +0,0 @@
angular.module('portainer.app').directive('rdLoading', function rdLoading() {
var directive = {
restrict: 'AE',
template: '<div class="loading"><div class="double-bounce1"></div><div class="double-bounce2"></div></div>',
};
return directive;
});

View File

@ -1,13 +0,0 @@
angular.module('portainer.app').directive('rdWidgetBody', function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
scope: {
loading: '@?',
classes: '@?',
},
transclude: true,
template: '<div class="widget-body" ng-class="classes"><rd-loading ng-show="loading"></rd-loading><div ng-hide="loading" class="widget-content" ng-transclude></div></div>',
restrict: 'E',
};
return directive;
});

View File

@ -1,14 +0,0 @@
angular.module('portainer.app').directive('rdWidgetCustomHeader', function rdWidgetCustomHeader() {
var directive = {
requires: '^rdWidget',
scope: {
titleText: '=',
icon: '=',
},
transclude: true,
template:
'<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}" ng-if="icon"></img><i class="fa fa-rocket" aria-hidden="true" ng-if="!icon"></i> <span class="text-muted"> {{titleText}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
restrict: 'E',
};
return directive;
});

View File

@ -1,9 +0,0 @@
angular.module('portainer.app').directive('rdWidgetFooter', function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
transclude: true,
template: '<div class="widget-footer" ng-transclude></div>',
restrict: 'E',
};
return directive;
});

View File

@ -1,26 +0,0 @@
angular.module('portainer.app').directive('rdWidgetHeader', function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
scope: {
titleText: '@',
icon: '@',
classes: '@?',
},
transclude: {
title: '?headerTitle',
},
template: `
<div class="widget-header">
<div class="row">
<span ng-class="classes" class="pull-left">
<i class="fa" ng-class="icon"></i>
<span ng-transclude="title">{{ titleText }}</span>
</span>
<span ng-class="classes" class="pull-right" ng-transclude></span>
</div>
</div>
`,
restrict: 'E',
};
return directive;
});

View File

@ -1,12 +0,0 @@
angular.module('portainer.app').directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
var directive = {
requires: '^rdWidget',
scope: {
classes: '@?',
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div ng-class="classes" ng-transclude></div></div></div>',
restrict: 'E',
};
return directive;
});

View File

@ -1,11 +0,0 @@
angular.module('portainer.app').directive('rdWidget', function rdWidget() {
var directive = {
scope: {
ngModel: '=',
},
transclude: true,
template: '<div class="widget" ng-transclude></div>',
restrict: 'EA',
};
return directive;
});

View File

@ -0,0 +1,12 @@
import { react2angular } from '@/react-tools/react2angular';
export function Loading() {
return (
<div className="loading">
<div className="double-bounce1" />
<div className="double-bounce2" />
</div>
);
}
export const LoadingAngular = react2angular(Loading, []);

View File

@ -0,0 +1,95 @@
import type { Meta } from '@storybook/react';
import { Widget } from './Widget';
import { WidgetBody } from './WidgetBody';
import { WidgetTitle } from './WidgetTitle';
import { WidgetFooter } from './WidgetFooter';
import { WidgetTaskbar } from './WidgetTaskbar';
interface WidgetProps {
loading: boolean;
title: string;
icon: string;
bodyText: string;
footerText: string;
}
const meta: Meta<WidgetProps> = {
title: 'Widget',
component: Widget,
args: {
loading: false,
title: 'Title',
icon: 'fa-rocket',
bodyText: 'Body',
footerText: 'Footer',
},
};
export default meta;
export function Default({
loading,
bodyText,
footerText,
icon,
title,
}: WidgetProps) {
return (
<Widget>
<WidgetTitle title={title} icon={icon} />
<WidgetBody loading={loading}>{bodyText}</WidgetBody>
<WidgetFooter>{footerText}</WidgetFooter>
</Widget>
);
}
export function WidgetWithCustomImage({
loading,
bodyText,
footerText,
icon,
title,
}: WidgetProps) {
return (
<Widget>
<WidgetTitle
title={title}
icon={
<img
className="custom-header-ico space-right"
src={icon}
alt="header-icon"
/>
}
/>
<WidgetBody loading={loading}>{bodyText}</WidgetBody>
<WidgetFooter>{footerText}</WidgetFooter>
</Widget>
);
}
WidgetWithCustomImage.args = {
icon: 'https://via.placeholder.com/150',
};
export function WidgetWithTaskBar({
loading,
bodyText,
footerText,
icon,
title,
}: WidgetProps) {
return (
<Widget>
<WidgetTitle title={title} icon={icon} />
<WidgetTaskbar>
<button type="button" className="btn btn-primary">
Button
</button>
</WidgetTaskbar>
<WidgetBody loading={loading}>{bodyText}</WidgetBody>
<WidgetFooter>{footerText}</WidgetFooter>
</Widget>
);
}

View File

@ -0,0 +1,24 @@
import { createContext, PropsWithChildren, useContext } from 'react';
const Context = createContext<null | boolean>(null);
export function useWidgetContext() {
const context = useContext(Context);
if (context == null) {
throw new Error('Should be inside a Widget component');
}
}
export const rdWidget = {
transclude: true,
template: `<div class="widget" ng-transclude></div>`,
};
export function Widget({ children }: PropsWithChildren<unknown>) {
return (
<Context.Provider value>
<div className="widget">{children}</div>
</Context.Provider>
);
}

View File

@ -0,0 +1,39 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { useWidgetContext } from './Widget';
import { Loading } from './Loading';
export const rdWidgetBody = {
requires: '^rdWidget',
bindings: {
loading: '@?',
classes: '@?',
},
transclude: true,
template: `
<div class="widget-body" ng-class="$ctrl.classes">
<rd-loading ng-show="$ctrl.loading"></rd-loading>
<div ng-hide="$ctrl.loading" class="widget-content" ng-transclude></div>
</div>
`,
};
interface Props {
loading?: boolean;
className?: string;
}
export function WidgetBody({
loading,
className,
children,
}: PropsWithChildren<Props>) {
useWidgetContext();
return (
<div className={clsx(className, 'widget-body')}>
{loading ? <Loading /> : <div className="widget-content">{children}</div>}
</div>
);
}

View File

@ -0,0 +1,22 @@
export const rdWidgetCustomHeader = {
requires: '^rdWidget',
bindings: {
titleText: '=',
icon: '=',
},
transclude: true,
template: `
<div class="widget-header">
<div class="row">
<span class="pull-left">
<img class="custom-header-ico space-right" ng-src="{{$ctrl.icon}}" ng-if="$ctrl.icon" alt="header-icon"/>
<i class="fa fa-rocket" aria-hidden="true" ng-if="!$ctrl.icon"/>
<span class="text-muted"> {{$ctrl.titleText}} </span>
</span>
<span class="pull-right col-xs-6 col-sm-4" ng-transclude></span>
</div>
</div>
`,
};
// a react component wasn't created because WidgetTitle were adjusted to support a custom image

View File

@ -0,0 +1,17 @@
import { PropsWithChildren } from 'react';
import { useWidgetContext } from './Widget';
export const rdWidgetFooter = {
requires: '^rdWidget',
transclude: true,
template: `
<div class="widget-footer" ng-transclude></div>
`,
};
export function WidgetFooter({ children }: PropsWithChildren<unknown>) {
useWidgetContext();
return <div className="widget-footer">{children}</div>;
}

View File

@ -0,0 +1,37 @@
import { PropsWithChildren } from 'react';
import { useWidgetContext } from './Widget';
export const rdWidgetTaskbar = {
requires: '^rdWidget',
bindings: {
classes: '@?',
},
transclude: true,
template: `
<div class="widget-header">
<div class="row">
<div ng-class="$ctrl.classes" ng-transclude></div>
</div>
</div>
`,
};
interface Props {
className?: string;
}
export function WidgetTaskbar({
children,
className,
}: PropsWithChildren<Props>) {
useWidgetContext();
return (
<div className="widget-header">
<div className="row">
<div className={className}>{children}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,54 @@
import clsx from 'clsx';
import { PropsWithChildren, ReactNode } from 'react';
import { useWidgetContext } from './Widget';
export const rdWidgetTitle = {
requires: '^rdWidget',
bindings: {
titleText: '@',
icon: '@',
classes: '@?',
},
transclude: {
title: '?headerTitle',
},
template: `
<div class="widget-header">
<div class="row">
<span ng-class="classes" class="pull-left">
<i class="fa" ng-class="icon"></i>
<span ng-transclude="title">{{ titleText }}</span>
</span>
<span ng-class="classes" class="pull-right" ng-transclude></span>
</div>
</div>
`,
};
interface Props {
title: ReactNode;
icon: ReactNode;
className?: string;
}
export function WidgetTitle({
title,
icon,
className,
children,
}: PropsWithChildren<Props>) {
useWidgetContext();
return (
<div className="widget-header">
<div className="row">
<span className={clsx('pull-left', className)}>
{typeof icon === 'string' ? <i className={clsx('fa', icon)} /> : icon}
{title}
</span>
<span className={clsx('pull-right', className)}>{children}</span>
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import angular from 'angular';
import { LoadingAngular } from './Loading';
import { rdWidget, Widget } from './Widget';
import { rdWidgetBody, WidgetBody } from './WidgetBody';
import { rdWidgetCustomHeader } from './WidgetCustomHeader';
import { rdWidgetFooter, WidgetFooter } from './WidgetFooter';
import { rdWidgetTitle, WidgetTitle } from './WidgetTitle';
import { rdWidgetTaskbar, WidgetTaskbar } from './WidgetTaskbar';
export { Widget, WidgetBody, WidgetFooter, WidgetTitle, WidgetTaskbar };
export default angular
.module('portainer.shared.components.widget', [])
.component('rdLoading', LoadingAngular)
.component('rdWidget', rdWidget)
.component('rdWidgetBody', rdWidgetBody)
.component('rdWidgetCustomHeader', rdWidgetCustomHeader)
.component('rdWidgetFooter', rdWidgetFooter)
.component('rdWidgetHeader', rdWidgetTitle)
.component('rdWidgetTaskbar', rdWidgetTaskbar).name;