Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anchors to dashboard #1698

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/ui/components/DashKit/DashKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import pluginControl from './plugins/Control/Control';
import pluginGroupControl from './plugins/GroupControl/GroupControl';
import {pluginImage} from './plugins/Image/Image';
import textPlugin from './plugins/Text/Text';
import pluginTitle from './plugins/Title/Title';
import getTitlePlugin from './plugins/Title/Title';
import widgetPlugin from './plugins/Widget/WidgetPlugin';

let isConfigured = false;
Expand All @@ -33,15 +33,18 @@ const wrapPlugins = (plugins: Plugin[], pluginDefaultsGetter?: typeof currentDef
});
};

export const getConfiguredDashKit = (pluginDefaultsGetter: typeof currentDefaultsGetter = null) => {
export const getConfiguredDashKit = (
pluginDefaultsGetter: typeof currentDefaultsGetter = null,
disableHashNavigation?: boolean,
Copy link
Contributor

@mournfulCoroner mournfulCoroner Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use object for props here
upd: Maybe only for second argument (I saw that it breaks compatibility)

) => {
const controlSettings = {
getDistincts: getDistinctsAction(),
};

if (currentDefaultsGetter !== pluginDefaultsGetter || !isConfigured) {
const plugins = wrapPlugins(
[
pluginTitle,
getTitlePlugin(disableHashNavigation),
textPlugin.setSettings({
apiHandler: MarkdownProvider.getMarkdown,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
.dashkit-plugin-container {
&__anchor {
opacity: 0;
right: 100%;
padding-right: 4px;
height: 100%;
float: left;

&_visible,
&:hover,
&:focus-visible {
opacity: 1;
}

&_absolute {
position: absolute;
}

&_size_l {
padding-top: 5px;
font-size: 24px;
line-height: 28px;
}

&_size_m {
padding-top: 7px;
font-size: 20px;
line-height: 24px;
}

&_size_s {
padding-top: 7px;
font-size: var(--g-text-body-3-font-size);
line-height: 24px;
}

&_size_xs {
padding-top: 9px;
font-size: 15px;
line-height: 20px;
}
}

&__wrapper {
height: 100%;

&:hover .dashkit-plugin-container__anchor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class doesn't work because anchor is placed outside the .dashkit-plugin-container__wrapper
I suggest putting the anchor classes in a separate file

opacity: 1;
}

&_widget {
width: 100%;
}
Expand All @@ -11,7 +57,8 @@
}

&_title {
overflow: hidden;
scroll-margin-left: calc(24px + var(--gn-aside-header-size, 0px));
overflow: clip;
display: flex;
align-items: center;
max-height: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@ type RendererProps = {
nodeRef?: React.RefObject<HTMLDivElement>;
classMod?: string;
style?: React.CSSProperties;
childContent?: React.ReactNode;
};

export const RendererWrapper: React.FC<RendererProps> = React.memo(
({children, type, nodeRef, classMod, ...props}) => {
({children, type, nodeRef, classMod, childContent, ...props}) => {
const innerNodeRef = React.useRef(null);
useWidgetContext(props.id, nodeRef || innerNodeRef);

return (
<div
ref={nodeRef || innerNodeRef}
className={b('wrapper', {
[type]: Boolean(type),
[String(classMod)]: Boolean(classMod),
})}
{...props}
>
{children}
</div>
<React.Fragment>
{childContent}
<div
ref={nodeRef || innerNodeRef}
className={b('wrapper', {
[type]: Boolean(type),
[String(classMod)]: Boolean(classMod),
})}
{...props}
>
{children}
</div>
</React.Fragment>
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import type {PluginTitleProps} from '@gravity-ui/dashkit';
import {Link} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
import {useHistory, useLocation} from 'react-router';

const b = block('dashkit-plugin-container');

interface DashAnchorLinkProps {
size: PluginTitleProps['data']['size'];
x: number;
to: string;
}

const DashAnchorLink = ({size, x, to}: DashAnchorLinkProps) => {
const location = useLocation();
const history = useHistory();
const hash = `#${encodeURIComponent(to)}`;
const isLinkVisible = false || location.hash === hash;

return (
<Link
onClick={(e) => {
e.preventDefault();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a link, then it should open in a new tab by cmd + click (the table of contents does so)

history.push({...location, hash});
}}
className={b('anchor', {
size,
absolute: x === 0,
visible: isLinkVisible,
})}
href={hash}
>
#
</Link>
);
};

export default DashAnchorLink;
4 changes: 1 addition & 3 deletions src/ui/components/DashKit/plugins/Title/Title.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
.dashkit-plugin-title-container {
height: fit-content;
overflow: hidden;
overflow: clip;

&_with-auto-height {
overflow-y: auto;

[data-plugin-root-el='title'] {
height: fit-content;
}
Expand Down
14 changes: 11 additions & 3 deletions src/ui/components/DashKit/plugins/Title/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import {useBeforeLoad} from '../../../../hooks/useBeforeLoad';
import {RendererWrapper} from '../RendererWrapper/RendererWrapper';

import DashAnchorLink from './DashAnchorLink/DashAnchorLink';

import './Title.scss';

const b = block('dashkit-plugin-title-container');
Expand All @@ -21,7 +23,7 @@ type Props = PluginTitleProps;

const WIDGET_RESIZE_DEBOUNCE_TIMEOUT = 100;

const titlePlugin = {
const getTitlePlugin = (disableHashNavigation?: boolean) => ({
...pluginTitle,
renderer: function Wrapper(
props: Props,
Expand Down Expand Up @@ -97,10 +99,16 @@ const titlePlugin = {
data.text,
]);

const anchor =
disableHashNavigation || props.editMode ? null : (
<DashAnchorLink size={data.size} x={currentLayout.x || 0} to={data.text} />
);

return (
<RendererWrapper
id={props.id}
type="title"
childContent={anchor}
nodeRef={rootNodeRef}
style={style as React.StyleHTMLAttributes<HTMLDivElement>}
classMod={classMod}
Expand All @@ -116,6 +124,6 @@ const titlePlugin = {
</RendererWrapper>
);
},
};
});

export default titlePlugin;
export default getTitlePlugin;
13 changes: 13 additions & 0 deletions src/ui/hooks/useUpdateEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

export const useUpdateEffect = (effect: React.EffectCallback, deps: React.DependencyList) => {
const isInitialMount = React.useRef(true);

React.useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
return effect();
}
}, deps);
};
1 change: 1 addition & 0 deletions src/ui/styles/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ body,
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
}

html,
Expand Down
25 changes: 20 additions & 5 deletions src/ui/units/dash/containers/Body/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import {getIsAsideHeaderEnabled} from '../../../../components/AsideHeaderAdapter
import {getConfiguredDashKit} from '../../../../components/DashKit/DashKit';
import {DL} from '../../../../constants';
import type SDK from '../../../../libs/sdk';
import Utils from '../../../../utils';
import Utils, {scrollToHash} from '../../../../utils';
import {TYPES_TO_DIALOGS_MAP, getActionPanelItems} from '../../../../utils/getActionPanelItems';
import {EmptyState} from '../../components/EmptyState/EmptyState';
import Loader from '../../components/Loader/Loader';
Expand Down Expand Up @@ -152,6 +152,7 @@ type DashBodyState = {
loaded: boolean;
prevMeta: {tabId: string | null; entryId: string | null};
loadedItemsMap: Map<string, boolean>;
hash: string;
};

type BodyProps = StateProps & DispatchProps & RouteComponentProps & OwnProps;
Expand Down Expand Up @@ -180,18 +181,27 @@ const GROUPS_WEIGHT = {
// Body is used as a core in different environments
class Body extends React.PureComponent<BodyProps> {
static getDerivedStateFromProps(props: BodyProps, state: DashBodyState) {
let newState: Partial<DashBodyState> = {};

const {
prevMeta: {entryId, tabId},
} = state;

// reset loaded before new tab/entry items are mounted
if (props.entryId !== entryId || props.tabId !== tabId) {
state.loadedItemsMap.clear();
newState = {
prevMeta: {tabId: props.tabId, entryId: props.entryId},
loaded: false,
};
}

return {prevMeta: {tabId: props.tabId, entryId: props.entryId}, loaded: false};
const newHash = props.location.hash;
if (newHash !== state.hash) {
newState.hash = newHash;
scrollToHash({hash: newHash.replace('#', ''), withDelay: props.tabId !== tabId});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do hash.replace('#', '') inside scrollToHash. It seems that it is not necessary to do this here?

}

return null;
return Object.keys(newState).length ? newState : null;
}

dashKitRef = React.createRef<DashKitComponent>();
Expand Down Expand Up @@ -250,6 +260,7 @@ class Body extends React.PureComponent<BodyProps> {
prevMeta: {tabId: null, entryId: null},
loaded: false,
loadedItemsMap: new Map<string, boolean>(),
hash: '',
};

groups: DashKitGroup[] = [
Expand Down Expand Up @@ -289,6 +300,8 @@ class Body extends React.PureComponent<BodyProps> {
// if localStorage already have a dash item, we need to set it to state
this.storageHandler();

scrollToHash({hash: this.props.location.hash, withDelay: true, checkUserScroll: true});

window.addEventListener('storage', this.storageHandler);
}

Expand Down Expand Up @@ -853,14 +866,16 @@ class Body extends React.PureComponent<BodyProps> {
isEditModeLoading,
globalParams,
dashkitSettings,
disableHashNavigation,
} = this.props;

const tabDataConfig = DL.IS_MOBILE
? this.getMobileLayout()
: (tabData as DashKitProps['config'] | null);

const isEmptyTab = !tabDataConfig?.items.length;
const DashKit = getConfiguredDashKit();

const DashKit = getConfiguredDashKit(undefined, disableHashNavigation);

return isEmptyTab && !isGlobalDragging ? (
<EmptyState
Expand Down
21 changes: 2 additions & 19 deletions src/ui/units/dash/containers/TableOfContent/TableOfContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {useDispatch} from 'react-redux';
import {Link, useLocation} from 'react-router-dom';
import {TableOfContentQa} from 'shared';
import {DL} from 'ui/constants';
import {scrollToHash} from 'ui/utils';
import {
selectHashStates,
selectShowTableOfContent,
selectTabId,
selectTabs,
} from 'units/dash/store/selectors/dashTypedSelectors';
import {scrollIntoView} from 'utils';

import {
appendSearchQuery,
Expand All @@ -33,9 +33,7 @@ const i18n = I18n.keyset('dash.table-of-content.view');

const b = block('table-of-content');

const scrollIntoViewOptions: ScrollIntoViewOptions = {behavior: 'smooth'};
const dispatchResizeTimeout = 200;
const scrollDelay = 300;

const getHash = ({
itemTitle,
Expand All @@ -53,16 +51,6 @@ const getHash = ({
return itemTitle ? `#${encodeURIComponent(itemTitle)}` : '';
};

const scrollIntoViewWithTimeout = (itemId: string) => {
setTimeout(
() => scrollIntoView(itemId, scrollIntoViewOptions),
// to have time to change the height of the react-grid-layout (200ms)
// DashKit rendering ended after location change (with manual page refresh) (50-70ms)
// small margin
scrollDelay,
);
};

const TableOfContent: React.FC<{disableHashNavigation?: boolean}> = React.memo(
({disableHashNavigation}) => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -98,7 +86,7 @@ const TableOfContent: React.FC<{disableHashNavigation?: boolean}> = React.memo(
handleToggleTableOfContent();
}
if (disableHashNavigation) {
scrollIntoViewWithTimeout(encodeURIComponent(itemTitle));
scrollToHash({hash: `#${encodeURIComponent(itemTitle)}`, withDelay: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding '#' is not necessary, is it?

}
},
[isSelectedTab, disableHashNavigation, dispatch, handleToggleTableOfContent],
Expand All @@ -125,11 +113,6 @@ const TableOfContent: React.FC<{disableHashNavigation?: boolean}> = React.memo(
[disableHashNavigation, hashStates, isSelectedTab, location],
);

React.useEffect(() => {
if (location.hash && !disableHashNavigation) {
scrollIntoViewWithTimeout(location.hash.replace('#', ''));
}
}, [location.hash, disableHashNavigation]);
React.useEffect(() => {
// to recalculate ReactGridLayout
dispatchResize(dispatchResizeTimeout);
Expand Down
Loading
Loading