Skip to content

Commit

Permalink
feat(dashboard): full view dashboard card (#905)
Browse files Browse the repository at this point in the history
* feat(dashboard): full view dashboard card

Signed-off-by: Thuan Vo <[email protected]>

* chore(dashboard): clean up

---------

Signed-off-by: Thuan Vo <[email protected]>
  • Loading branch information
tthvo authored Mar 13, 2023
1 parent bfd9f86 commit c704ceb
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 284 deletions.
3 changes: 2 additions & 1 deletion locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"CANCEL": "Cancel",
"CARD_TYPE": "Card type",
"CLEAR_FILTERS": "Clear all filters",
"CRITICAL": "CRITICAL",
"CREATE": "Create",
"CREATING": "Creating",
"CRITICAL": "CRITICAL",
"CRYOSTAT_TRADEMARK": "Copyright The Cryostat Authors, The Universal Permissive License (UPL), Version 1.0",
"DATE": "Date",
"DESCRIPTION": "Description",
Expand Down Expand Up @@ -47,5 +47,6 @@
"TEMPLATE": "Template",
"TEMPLATE_HELPER_TEXT_INVALID": "Template must be selected",
"TIME": "Time",
"VIEW": "View",
"WARNING": "WARNING"
}
2 changes: 1 addition & 1 deletion src/app/BreadcrumbPage/BreadcrumbPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ export const BreadcrumbPage: React.FC<BreadcrumbPageProps> = (props) => {
export const isItemFilled = (item: React.ReactNode): boolean => {
if (!item) return false;
const toCheck = item['props'] ? item['props'] : item;
return toCheck['isFilled'] || toCheck['isFullHeight'];
return toCheck['isFilled'] || toCheck['isFullHeight'] || toCheck['data-full-height'];
};
3 changes: 3 additions & 0 deletions src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,9 @@ export const AutomatedAnalysisCard: React.FC<AutomatedAnalysisCardProps> = (prop
cardSizes={AutomatedAnalysisCardSizes}
id="automated-analysis-card"
isCompact
isDraggable={props.isDraggable}
isResizable={props.isResizable}
isFullHeight={props.isFullHeight}
isExpanded={isCardExpanded}
cardHeader={header}
>
Expand Down
13 changes: 8 additions & 5 deletions src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,20 @@ export const JFRMetricsChartCard: React.FC<JFRMetricsChartCardProps> = (props) =
}, [chartSrc, dashboardUrl]);

const cardStyle = React.useMemo(() => {
if (controllerState !== ControllerState.READY) {
if (controllerState !== ControllerState.READY || props.isFullHeight) {
return {};
}
let height: number;
let height: string;
switch (props.chartKind) {
case 'Core Count':
height = 250;
height = '250px';
break;
default:
height = 380;
height = `380px`;
break;
}
return { height };
}, [controllerState, props.chartKind]);
}, [controllerState, props.chartKind, props.isFullHeight]);

const resyncButton = React.useMemo(() => {
return (
Expand Down Expand Up @@ -255,6 +255,9 @@ export const JFRMetricsChartCard: React.FC<JFRMetricsChartCardProps> = (props) =
isCompact
style={cardStyle}
cardHeader={header}
isDraggable={props.isDraggable}
isResizable={props.isResizable}
isFullHeight={props.isFullHeight}
>
<CardBody>
{controllerState === ControllerState.UNKNOWN ? (
Expand Down
9 changes: 7 additions & 2 deletions src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const SimpleChart: React.FC<{
legendPosition={'bottom'}
themeColor={themeColor}
width={width}
height={width / 2} // Aspect radio: 2:1
padding={{
left: 54,
right: 30,
Expand Down Expand Up @@ -270,6 +271,7 @@ const chartKinds: MBeanMetricsChartKind[] = [
labels={({ datum }) => (datum.x ? `${datum.x}: ${datum.y.toFixed(2)}%` : null)}
themeColor={themeColor}
width={width}
height={width / 2} // Aspect radio: 2:1
/>
);
},
Expand Down Expand Up @@ -422,11 +424,11 @@ export const MBeanMetricsChartCard: React.FC<MBeanMetricsChartCardProps> = (prop

const visual = React.useMemo(
() => (
<div ref={containerRef} style={{ height: 300 }} className="disabled-pointer">
<div ref={containerRef} style={{ height: props.isFullHeight ? '100%' : '300px' }} className="disabled-pointer">
{chartKind.visual(props.themeColor, cardWidth, samples)}
</div>
),
[containerRef, props.themeColor, chartKind, cardWidth, samples]
[containerRef, props.themeColor, props.isFullHeight, chartKind, cardWidth, samples]
);

return (
Expand All @@ -435,6 +437,9 @@ export const MBeanMetricsChartCard: React.FC<MBeanMetricsChartCardProps> = (prop
dashboardId={props.dashboardId}
cardSizes={MBeanMetricsChartCardSizes}
isCompact
isDraggable={props.isDraggable}
isResizable={props.isResizable}
isFullHeight={props.isFullHeight}
cardHeader={header}
>
<CardBody>{visual}</CardBody>
Expand Down
103 changes: 103 additions & 0 deletions src/app/Dashboard/DashBoardSolo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright The Cryostat Authors
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or data
* (collectively the "Software"), free of charge and under any and all copyright
* rights in the Software, and any and all patent rights owned or freely
* licensable by each licensor hereunder covering either (i) the unmodified
* Software as contributed to or provided by such licensor, or (ii) the Larger
* Works (as defined below), to deal in both
*
* (a) the Software, and
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software (each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
* The above copyright notice and either this complete permission notice or at
* a minimum a reference to the UPL must be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { CardConfig } from '@app/Shared/Redux/Configurations/DashboardConfigSlicer';
import { RootState } from '@app/Shared/Redux/ReduxStore';
import { TargetView } from '@app/TargetView/TargetView';
import { Bullseye, Button, EmptyState, EmptyStateBody, EmptyStateIcon, Title } from '@patternfly/react-core';
import { MonitoringIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { useSelector } from 'react-redux';
import { useHistory, useLocation, withRouter } from 'react-router-dom';
import { getConfigByName } from './Dashboard';

export interface DashboardSoloProps {}

const DashboardSolo: React.FC<DashboardSoloProps> = ({ ..._props }) => {
const { search } = useLocation();
const history = useHistory();

const cardConfigs: CardConfig[] = useSelector((state: RootState) => state.dashboardConfigs.list);

const cardConfig = React.useMemo(() => {
const cardId = new URLSearchParams(search).get('cardId');
return cardConfigs.find((config) => config.id === cardId);
}, [search, cardConfigs]);

const content = React.useMemo(() => {
if (!cardConfig) {
return (
<Bullseye>
<EmptyState variant="large">
<EmptyStateIcon variant="container" component={MonitoringIcon} />
<Title headingLevel="h3" size="lg">
Dashboard card not found
</Title>
<EmptyStateBody>
Provide a valid <code style={{ color: '#000' }}>cardId</code> query parameter and try again.
</EmptyStateBody>
<Button variant="primary" onClick={() => history.push('/')}>
Back to Dashboard
</Button>
</EmptyState>
</Bullseye>
);
} else {
const { id, name, span, props } = cardConfig;
return (
// Use default chart controller
<TargetView pageTitle={cardConfig.id} breadcrumbs={[{ path: '/', title: 'Dashboard' }]}>
<div data-full-height style={{ height: '100%' }}>
{React.createElement(getConfigByName(name).component, {
span: span,
...props,
isDraggable: false,
isResizable: false,
isFullHeight: true,
dashboardId: id,
})}
</div>
<></>
</TargetView>
);
}
}, [cardConfig, history]);

return content;
};

export default withRouter(DashboardSolo);
6 changes: 6 additions & 0 deletions src/app/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { TFunction } from 'i18next';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { Observable, of } from 'rxjs';
import { AddCard } from './AddCard';
import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAnalysisCard';
Expand Down Expand Up @@ -101,6 +102,9 @@ export interface DashboardProps {}
export interface DashboardCardProps {
span: number;
dashboardId: number;
isDraggable?: boolean;
isResizable?: boolean;
isFullHeight?: boolean;
actions?: JSX.Element[];
}

Expand Down Expand Up @@ -286,6 +290,7 @@ export function getConfigByTitle(title: string, t: TFunction): DashboardCardDesc
}

export const Dashboard: React.FC<DashboardProps> = (_) => {
const history = useHistory();
const serviceContext = React.useContext(ServiceContext);
const dispatch = useDispatch<StateDispatch>();
const cardConfigs: CardConfig[] = useSelector((state: RootState) => state.dashboardConfigs.list);
Expand Down Expand Up @@ -361,6 +366,7 @@ export const Dashboard: React.FC<DashboardProps> = (_) => {
key={`${cfg.name}-actions`}
onRemove={() => handleRemove(idx)}
onResetSize={() => handleResetSize(idx)}
onView={() => history.push(`/d-solo?cardId=${cfg.id}`)}
/>,
],
})}
Expand Down
55 changes: 38 additions & 17 deletions src/app/Dashboard/DashboardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,19 @@ export interface DashboardCardProps extends CardProps {
dashboardId: number;
cardSizes: DashboardCardSizes;
cardHeader: React.ReactNode;
isDraggable?: boolean;
isResizable?: boolean;
children?: React.ReactNode;
}

export const DashboardCard: React.FC<DashboardCardProps> = ({
children = null,
cardHeader = null,
dashboardId,
isDraggable = true,
isResizable = true,
cardSizes,

...props
}: DashboardCardProps) => {
const cardRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -73,25 +78,41 @@ export const DashboardCard: React.FC<DashboardCardProps> = ({
}
}, []);

return (
<DashboardCardContext.Provider value={cardRef}>
<DraggableRef dashboardId={dashboardId}>
<div className={'dashboard-card-resizable-wrapper'} ref={cardRef}>
<Card className="dashboard-card" isRounded {...props}>
<div
className={css(`${draggableRefKlazz}__grip`)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
draggable // draggable is required for drag events to fire
>
{cardHeader}
</div>
const resizeBar = React.useMemo(() => {
return isResizable ? <ResizableRef dashboardId={dashboardId} cardSizes={cardSizes} /> : null;
}, [isResizable, cardSizes, dashboardId]);

const content = React.useMemo(
() =>
isDraggable ? (
<DraggableRef dashboardId={dashboardId}>
<div className={'dashboard-card-resizable-wrapper'} ref={cardRef}>
<Card className="dashboard-card" isRounded {...props}>
<div
className={css(`${draggableRefKlazz}__grip`)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
draggable // draggable is required for drag events to fire
>
{cardHeader}
</div>
{children}
</Card>
{resizeBar}
</div>
</DraggableRef>
) : (
<>
<Card isRounded {...props}>
{cardHeader}
{children}
</Card>
<ResizableRef dashboardId={dashboardId} cardSizes={cardSizes} />
</div>
</DraggableRef>
</DashboardCardContext.Provider>
{resizeBar}
</>
),
[cardRef, props, onMouseEnter, onMouseLeave, cardHeader, children, isDraggable, dashboardId, resizeBar]
);

return <DashboardCardContext.Provider value={cardRef}>{content}</DashboardCardContext.Provider>;
};
DashboardCard.displayName = 'DashboardCard';
16 changes: 13 additions & 3 deletions src/app/Dashboard/DashboardCardActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ import { useTranslation } from 'react-i18next';

export interface DashboardCardActionProps {
onRemove: () => void;
onView: () => void;
onResetSize: () => void;
}

export const DashboardCardActionMenu: React.FunctionComponent<DashboardCardActionProps> = (props) => {
export const DashboardCardActionMenu: React.FunctionComponent<DashboardCardActionProps> = ({
onRemove,
onResetSize,
onView,
...props
}) => {
const [isOpen, setOpen] = React.useState(false);

const [t] = useTranslation();
Expand All @@ -59,6 +65,7 @@ export const DashboardCardActionMenu: React.FunctionComponent<DashboardCardActio
return (
<>
<Dropdown
{...props}
isPlain
isFlipEnabled
menuAppendTo={'parent'}
Expand All @@ -67,10 +74,13 @@ export const DashboardCardActionMenu: React.FunctionComponent<DashboardCardActio
toggle={<KebabToggle onToggle={setOpen} />}
onSelect={onSelect}
dropdownItems={[
<DropdownItem key="Remove" onClick={props.onRemove}>
<DropdownItem key="View" onClick={onView}>
{t('VIEW', { ns: 'common' })}
</DropdownItem>,
<DropdownItem key="Remove" onClick={onRemove}>
{t('REMOVE', { ns: 'common' })}
</DropdownItem>,
<DropdownItem key="Reset Size" onClick={props.onResetSize}>
<DropdownItem key="Reset Size" onClick={onResetSize}>
{t('DashboardCardActionMenu.RESET_SIZE')}
</DropdownItem>,
]}
Expand Down
3 changes: 3 additions & 0 deletions src/app/Dashboard/Quickstart/QuickStartsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ const QuickStartsCard: React.FunctionComponent<QuickStartsCardProps> = (props) =
<DashboardCard
dashboardId={props.dashboardId}
cardSizes={QuickStartsCardSizes}
isDraggable={props.isDraggable}
isResizable={props.isResizable}
isFullHeight={props.isFullHeight}
cardHeader={
<CardHeader>
<CardTitle component="h2">{t('QuickStartsCard.TITLE')}</CardTitle>
Expand Down
1 change: 0 additions & 1 deletion src/app/Shared/MatchExpressionEvaluator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import {
InfoCircleIcon,
WarningTriangleIcon,
} from '@patternfly/react-icons';
import _ from 'lodash';
import * as React from 'react';

export interface MatchExpressionEvaluatorProps {
Expand Down
1 change: 0 additions & 1 deletion src/app/Topology/Shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import { TopologyFilters } from '@app/Shared/Redux/Filters/TopologyFilterSlice';
import { evaluateTargetWithExpr, hashCode } from '@app/utils/utils';
import { Button } from '@patternfly/react-core';
import { ContextMenuSeparator, GraphElement, NodeStatus } from '@patternfly/react-topology';
import _ from 'lodash';
import * as React from 'react';
import { BehaviorSubject, debounceTime, Observable, Subscription } from 'rxjs';
import { ContextMenuItem, MenuItemVariant, nodeActions } from '../Actions/NodeActions';
Expand Down
Loading

0 comments on commit c704ceb

Please sign in to comment.