mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Adds initial section and Page to Dashboard submenu (#32876)
* LibraryPanels: Adds initial section and Page to Dashboard submenu * Refactor: adds perPage as prop * Chore: renames OrgActionBar * Chore: updates after PR comments * Chore: updates snapshot
This commit is contained in:
parent
0c71fdac3d
commit
7d07599dc1
@ -165,6 +165,12 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
Url: hs.Cfg.AppSubURL + "/dashboard/snapshots",
|
||||
Icon: "camera",
|
||||
})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||
Text: "Global panels",
|
||||
Id: "library-panels",
|
||||
Url: hs.Cfg.AppSubURL + "/library-panels",
|
||||
Icon: "reusable-panel",
|
||||
})
|
||||
}
|
||||
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import OrgActionBar, { Props } from './OrgActionBar';
|
||||
import PageActionBar, { Props } from './PageActionBar';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -12,7 +12,7 @@ const setup = (propOverrides?: object) => {
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<OrgActionBar {...props} />);
|
||||
return shallow(<PageActionBar {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
@ -5,14 +5,15 @@ import { LinkButton } from '@grafana/ui';
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
setSearchQuery: (value: string) => void;
|
||||
linkButton: { href: string; title: string };
|
||||
linkButton?: { href: string; title: string };
|
||||
target?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export default class OrgActionBar extends PureComponent<Props> {
|
||||
export default class PageActionBar extends PureComponent<Props> {
|
||||
render() {
|
||||
const { searchQuery, linkButton, setSearchQuery, target } = this.props;
|
||||
const linkProps = { href: linkButton.href };
|
||||
const { searchQuery, linkButton, setSearchQuery, target, placeholder = 'Search by name or type' } = this.props;
|
||||
const linkProps = { href: linkButton?.href };
|
||||
|
||||
if (target) {
|
||||
(linkProps as any).target = target;
|
||||
@ -21,10 +22,10 @@ export default class OrgActionBar extends PureComponent<Props> {
|
||||
return (
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<FilterInput value={searchQuery} onChange={setSearchQuery} placeholder={'Search by name or type'} />
|
||||
<FilterInput value={searchQuery} onChange={setSearchQuery} placeholder={placeholder} />
|
||||
</div>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<LinkButton {...linkProps}>{linkButton.title}</LinkButton>
|
||||
{linkButton && <LinkButton {...linkProps}>{linkButton.title}</LinkButton>}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -12,3 +12,5 @@ export const LS_PANEL_COPY_KEY = 'panel-copy';
|
||||
export const PANEL_BORDER = 2;
|
||||
|
||||
export const EDIT_PANEL_ID = 23763571993;
|
||||
|
||||
export const DEFAULT_PER_PAGE_PAGINATION = 8;
|
||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
// Components
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import DataSourcesList from './DataSourcesList';
|
||||
// Types
|
||||
@ -75,7 +75,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
|
||||
{hasFetched && dataSourcesCount === 0 && <EmptyListCTA {...emptyListModel} />}
|
||||
{hasFetched &&
|
||||
dataSourcesCount > 0 && [
|
||||
<OrgActionBar
|
||||
<PageActionBar
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={(query) => setDataSourcesSearchQuery(query)}
|
||||
linkButton={linkButton}
|
||||
|
@ -16,7 +16,7 @@ exports[`Render should render action bar and datasources 1`] = `
|
||||
<PageContents
|
||||
isLoading={false}
|
||||
>
|
||||
<OrgActionBar
|
||||
<PageActionBar
|
||||
key="action-bar"
|
||||
linkButton={
|
||||
Object {
|
||||
|
49
public/app/features/library-panels/LibraryPanelsPage.tsx
Normal file
49
public/app/features/library-panels/LibraryPanelsPage.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
|
||||
import { StoreState } from '../../types';
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import Page from '../../core/components/Page/Page';
|
||||
import { LibraryPanelsView } from './components/LibraryPanelsView/LibraryPanelsView';
|
||||
import { useAsync } from 'react-use';
|
||||
import { getLibraryPanels } from './state/api';
|
||||
import PageActionBar from '../../core/components/PageActionBar/PageActionBar';
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
navModel: getNavModel(state.navIndex, 'library-panels'),
|
||||
});
|
||||
|
||||
const connector = connect(mapStateToProps, undefined);
|
||||
|
||||
interface OwnProps extends GrafanaRouteComponentProps {}
|
||||
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export const LibraryPanelsPage: FC<Props> = ({ navModel }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { value: searchResult, loading } = useAsync(async () => {
|
||||
return getLibraryPanels();
|
||||
});
|
||||
const hasLibraryPanels = Boolean(searchResult?.libraryPanels.length);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={loading}>
|
||||
{hasLibraryPanels && (
|
||||
<PageActionBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} placeholder={'Search by name'} />
|
||||
)}
|
||||
<LibraryPanelsView
|
||||
onClickCard={() => undefined}
|
||||
searchString={searchQuery}
|
||||
currentPanelId={undefined}
|
||||
showSecondaryActions={true}
|
||||
perPage={DEFAULT_PER_PAGE_PAGINATION}
|
||||
/>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LibraryPanelsPage);
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useReducer } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Pagination, stylesFactory, useStyles } from '@grafana/ui';
|
||||
import { Pagination, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme, LoadingState } from '@grafana/data';
|
||||
|
||||
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
|
||||
@ -15,6 +15,7 @@ interface LibraryPanelViewProps {
|
||||
showSecondaryActions?: boolean;
|
||||
currentPanelId?: string;
|
||||
searchString: string;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
@ -23,6 +24,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
searchString,
|
||||
showSecondaryActions,
|
||||
currentPanelId: currentPanel,
|
||||
perPage: propsPerPage = 40,
|
||||
}) => {
|
||||
const styles = useStyles(getPanelViewStyles);
|
||||
const [{ libraryPanels, page, perPage, numberOfPages, loadingState, currentPanelId }, dispatch] = useReducer(
|
||||
@ -30,6 +32,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
{
|
||||
...initialLibraryPanelsViewState,
|
||||
currentPanelId: currentPanel,
|
||||
perPage: propsPerPage,
|
||||
}
|
||||
);
|
||||
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
||||
@ -75,7 +78,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const getPanelViewStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
@ -96,6 +99,7 @@ const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
`,
|
||||
pagination: css`
|
||||
align-self: center;
|
||||
margin-top: ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { MapStateToProps, connect } from 'react-redux';
|
||||
import { connect, MapStateToProps } from 'react-redux';
|
||||
import { NavModel, SelectableValue, urlUtil } from '@grafana/data';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { StoreState } from 'app/types';
|
||||
@ -10,7 +10,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { PlaylistDTO } from './types';
|
||||
import { Button, Card, Checkbox, Field, LinkButton, Modal, RadioButtonGroup, VerticalGroup } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
|
||||
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
|
||||
|
||||
interface ConnectedProps {
|
||||
@ -73,7 +73,7 @@ export const PlaylistPage: FC<Props> = ({ navModel }) => {
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={loading}>
|
||||
{hasPlaylists && (
|
||||
<OrgActionBar
|
||||
<PageActionBar
|
||||
searchQuery={searchQuery}
|
||||
linkButton={{ title: 'New playlist', href: '/playlists/new' }}
|
||||
setSearchQuery={setSearchQuery}
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
|
||||
import PluginList from './PluginList';
|
||||
import { loadPlugins } from './state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
@ -44,7 +44,7 @@ export const PluginListPage: React.FC<Props> = ({
|
||||
<Page navModel={navModel} aria-label={selectors.pages.PluginsList.page}>
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
<>
|
||||
<OrgActionBar
|
||||
<PageActionBar
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={(query) => setPluginsSearchQuery(query)}
|
||||
linkButton={linkButton}
|
||||
|
@ -447,6 +447,12 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "PlaylistEditPage"*/ 'app/features/playlist/PlaylistEditPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/library-panels',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/sandbox/benchmarks',
|
||||
component: SafeDynamicImport(
|
||||
|
Loading…
Reference in New Issue
Block a user