mirror of https://github.com/portainer/portainer
refactor(app/widgets): create widgets react components [EE-1813] (#6097)
parent
1a6af5d58f
commit
bcaf20caca
|
@ -307,7 +307,6 @@ a[ng-click] {
|
||||||
.custom-header-ico {
|
.custom-header-ico {
|
||||||
max-width: 32px;
|
max-width: 32px;
|
||||||
max-height: 32px;
|
max-height: 32px;
|
||||||
margin-right: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-responsive {
|
.btn-responsive {
|
||||||
|
|
|
@ -4,11 +4,12 @@ import sidebarModule from './sidebar';
|
||||||
import gitFormModule from './forms/git-form';
|
import gitFormModule from './forms/git-form';
|
||||||
import porAccessManagementModule from './accessManagement';
|
import porAccessManagementModule from './accessManagement';
|
||||||
import formComponentsModule from './form-components';
|
import formComponentsModule from './form-components';
|
||||||
|
import widgetModule from './widget';
|
||||||
|
|
||||||
import { ReactExampleAngular } from './ReactExample';
|
import { ReactExampleAngular } from './ReactExample';
|
||||||
import { TooltipAngular } from './Tooltip';
|
import { TooltipAngular } from './Tooltip';
|
||||||
|
|
||||||
export default angular
|
export default angular
|
||||||
.module('portainer.app.components', [sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
|
.module('portainer.app.components', [widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
|
||||||
.component('portainerTooltip', TooltipAngular)
|
.component('portainerTooltip', TooltipAngular)
|
||||||
.component('reactExample', ReactExampleAngular).name;
|
.component('reactExample', ReactExampleAngular).name;
|
||||||
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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, []);
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
Loading…
Reference in New Issue