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 {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-responsive {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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