LibraryElements: Adds library elements api and tables (#33741)

* WIP: intial structure

* Refactor: adds create library element endpoint

* Feature: adds delete library element

* wip

* Refactor: adds get api

* Refactor: adds get all api

* Refactor: adds patch api

* Refactor: changes to library_element_connection

* Refactor: add get connections api

* wip: in the middle of refactor

* wip

* Refactor: consolidating both api:s

* Refactor: points front end to library elements api

* Tests: Fixes broken test

* Fix: fixes delete library elements in folder and adds tests

* Refactor: changes order of tabs in manage folder

* Refactor: fixes so link does not cover whole card

* Update pkg/services/libraryelements/libraryelements.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/libraryelements/libraryelements_permissions_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/libraryelements/database.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Chore: changes after PR comments

* Update libraryelements.go

* Chore: updates after PR comments

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Hugo Häggmark
2021-05-11 07:10:19 +02:00
committed by GitHub
co-authored by Arve Knudsen
parent 9b12e79f3e
commit f1b2c750e5
42 changed files with 2758 additions and 2445 deletions
@@ -6,7 +6,7 @@ import { StoreState } from '../../types';
import { getNavModel } from '../../core/selectors/navModel';
import Page from '../../core/components/Page/Page';
import { LibraryPanelsSearch } from './components/LibraryPanelsSearch/LibraryPanelsSearch';
import { LibraryPanelDTO } from './types';
import { LibraryElementDTO } from './types';
import { OpenLibraryPanelModal } from './components/OpenLibraryPanelModal/OpenLibraryPanelModal';
const mapStateToProps = (state: StoreState) => ({
@@ -20,7 +20,7 @@ interface OwnProps extends GrafanaRouteComponentProps {}
type Props = OwnProps & ConnectedProps<typeof connector>;
export const LibraryPanelsPage: FC<Props> = ({ navModel }) => {
const [selected, setSelected] = useState<LibraryPanelDTO | undefined>(undefined);
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
return (
<Page navModel={navModel}>
@@ -2,14 +2,14 @@ import React, { FC, useEffect, useMemo, useReducer } from 'react';
import { Button, Modal, useStyles } from '@grafana/ui';
import { LoadingState } from '@grafana/data';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { asyncDispatcher } from '../LibraryPanelsView/actions';
import { deleteLibraryPanelModalReducer, initialDeleteLibraryPanelModalState } from './reducer';
import { getConnectedDashboards } from './actions';
import { getModalStyles } from '../../styles';
interface Props {
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
onConfirm: () => void;
onDismiss: () => void;
}
@@ -1,8 +1,8 @@
import { DispatchResult, LibraryPanelDTO } from '../../types';
import { DispatchResult, LibraryElementDTO } from '../../types';
import { getConnectedDashboards as apiGetConnectedDashboards } from '../../state/api';
import { searchCompleted } from './reducer';
export function getConnectedDashboards(libraryPanel: LibraryPanelDTO): DispatchResult {
export function getConnectedDashboards(libraryPanel: LibraryElementDTO): DispatchResult {
return async function (dispatch) {
const dashboards = await apiGetConnectedDashboards(libraryPanel.uid);
dispatch(searchCompleted({ dashboards }));
@@ -4,14 +4,14 @@ import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Icon, Link, useStyles2 } from '@grafana/ui';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { PanelTypeCard } from 'app/features/dashboard/components/VizTypePicker/PanelTypeCard';
import { DeleteLibraryPanelModal } from '../DeleteLibraryPanelModal/DeleteLibraryPanelModal';
export interface LibraryPanelCardProps {
libraryPanel: LibraryPanelDTO;
onClick: (panel: LibraryPanelDTO) => void;
onDelete?: (panel: LibraryPanelDTO) => void;
libraryPanel: LibraryElementDTO;
onClick: (panel: LibraryElementDTO) => void;
onDelete?: (panel: LibraryElementDTO) => void;
showSecondaryActions?: boolean;
}
@@ -54,7 +54,7 @@ export const LibraryPanelCard: React.FC<LibraryPanelCardProps & { children?: JSX
};
interface FolderLinkProps {
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
}
function FolderLink({ libraryPanel }: FolderLinkProps): JSX.Element {
@@ -64,18 +64,18 @@ function FolderLink({ libraryPanel }: FolderLinkProps): JSX.Element {
return (
<span className={styles.metaContainer}>
<Icon name={'folder'} size="sm" />
{libraryPanel.meta.folderName}
<span>{libraryPanel.meta.folderName}</span>
</span>
);
}
return (
<Link href={`/dashboards/f/${libraryPanel.meta.folderUid}`}>
<span className={styles.metaContainer}>
<span className={styles.metaContainer}>
<Link href={`/dashboards/f/${libraryPanel.meta.folderUid}`}>
<Icon name={'folder-upload'} size="sm" />
{libraryPanel.meta.folderName}
</span>
</Link>
<span>{libraryPanel.meta.folderName}</span>
</Link>
</span>
);
}
@@ -6,7 +6,7 @@ import { PanelPluginMeta, PluginType } from '@grafana/data';
import { LibraryPanelsSearch, LibraryPanelsSearchProps } from './LibraryPanelsSearch';
import * as api from '../../state/api';
import { LibraryPanelSearchResult } from '../../types';
import { LibraryElementKind, LibraryElementsSearchResult } from '../../types';
import { backendSrv } from '../../../../core/services/backend_srv';
import * as viztypepicker from '../../../dashboard/components/VizTypePicker/VizTypePicker';
@@ -38,7 +38,7 @@ jest.mock('debounce-promise', () => {
async function getTestContext(
propOverrides: Partial<LibraryPanelsSearchProps> = {},
searchResult: LibraryPanelSearchResult = { libraryPanels: [], perPage: 40, page: 1, totalCount: 0 }
searchResult: LibraryElementsSearchResult = { elements: [], perPage: 40, page: 1, totalCount: 0 }
) {
jest.clearAllMocks();
const pluginInfo: any = { logos: { small: '', large: '' } };
@@ -102,7 +102,7 @@ describe('LibraryPanelsSearch', () => {
searchString: 'a',
folderFilter: [],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@@ -130,7 +130,7 @@ describe('LibraryPanelsSearch', () => {
sortDirection: 'alpha-desc',
folderFilter: [],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@@ -158,7 +158,7 @@ describe('LibraryPanelsSearch', () => {
searchString: '',
folderFilter: [],
page: 0,
panelFilter: ['graph', 'timeseries'],
typeFilter: ['graph', 'timeseries'],
perPage: 40,
});
});
@@ -188,7 +188,7 @@ describe('LibraryPanelsSearch', () => {
searchString: '',
folderFilter: ['0'],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@@ -203,10 +203,11 @@ describe('LibraryPanelsSearch', () => {
page: 1,
totalCount: 1,
perPage: 40,
libraryPanels: [
elements: [
{
id: 1,
name: 'Library Panel Name',
kind: LibraryElementKind.Panel,
uid: 'uid',
description: 'Library Panel Description',
folderId: 0,
@@ -215,7 +216,6 @@ describe('LibraryPanelsSearch', () => {
orgId: 1,
version: 1,
meta: {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,
@@ -247,10 +247,11 @@ describe('LibraryPanelsSearch', () => {
page: 1,
totalCount: 1,
perPage: 40,
libraryPanels: [
elements: [
{
id: 1,
name: 'Library Panel Name',
kind: LibraryElementKind.Panel,
uid: 'uid',
description: 'Library Panel Description',
folderId: 0,
@@ -259,7 +260,6 @@ describe('LibraryPanelsSearch', () => {
orgId: 1,
version: 1,
meta: {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,
@@ -7,7 +7,7 @@ import { SortPicker } from '../../../../core/components/Select/SortPicker';
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter';
import { FolderInfo } from '../../../../types';
import {
@@ -25,7 +25,7 @@ export enum LibraryPanelsSearchVariant {
}
export interface LibraryPanelsSearchProps {
onClick: (panel: LibraryPanelDTO) => void;
onClick: (panel: LibraryElementDTO) => void;
variant?: LibraryPanelsSearchVariant;
showSort?: boolean;
showPanelFilter?: boolean;
@@ -5,13 +5,13 @@ import { Pagination, useStyles } from '@grafana/ui';
import { GrafanaTheme, LoadingState } from '@grafana/data';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer';
import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './actions';
interface LibraryPanelViewProps {
className?: string;
onClickCard: (panel: LibraryPanelDTO) => void;
onClickCard: (panel: LibraryElementDTO) => void;
showSecondaryActions?: boolean;
currentPanelId?: string;
searchString: string;
@@ -58,7 +58,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
300,
[searchString, sortDirection, panelFilter, folderFilter, page, asyncDispatch]
);
const onDelete = ({ uid }: LibraryPanelDTO) =>
const onDelete = ({ uid }: LibraryElementDTO) =>
asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
@@ -27,11 +27,11 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
page: args.page,
excludeUid: args.currentPanelId,
sortDirection: args.sortDirection,
panelFilter: args.panelFilter,
typeFilter: args.panelFilter,
folderFilter: args.folderFilter,
})
).pipe(
mergeMap(({ perPage, libraryPanels, page, totalCount }) =>
mergeMap(({ perPage, elements: libraryPanels, page, totalCount }) =>
of(searchCompleted({ libraryPanels, page, perPage, totalCount }))
),
catchError((err) => {
@@ -9,7 +9,7 @@ import {
LibraryPanelsViewState,
searchCompleted,
} from './reducer';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO, LibraryElementKind } from '../../types';
describe('libraryPanelsViewReducer', () => {
describe('when initSearch is dispatched', () => {
@@ -85,8 +85,8 @@ describe('libraryPanelsViewReducer', () => {
});
});
function getLibraryPanelMocks(count: number): LibraryPanelDTO[] {
const mocks: LibraryPanelDTO[] = [];
function getLibraryPanelMocks(count: number): LibraryElementDTO[] {
const mocks: LibraryElementDTO[] = [];
for (let i = 0; i < count; i++) {
mocks.push(
@@ -109,7 +109,6 @@ function mockLibraryPanel({
name = 'Test Panel',
model = { type: 'text', title: 'Test Panel' },
meta = {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,
@@ -121,13 +120,14 @@ function mockLibraryPanel({
version = 1,
description = 'a description',
type = 'text',
}: Partial<LibraryPanelDTO> = {}): LibraryPanelDTO {
}: Partial<LibraryElementDTO> = {}): LibraryElementDTO {
return {
uid,
id,
orgId,
folderId,
name,
kind: LibraryElementKind.Panel,
model,
version,
meta,
@@ -1,12 +1,12 @@
import { createAction } from '@reduxjs/toolkit';
import { LoadingState } from '@grafana/data';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { AnyAction } from 'redux';
export interface LibraryPanelsViewState {
loadingState: LoadingState;
libraryPanels: LibraryPanelDTO[];
libraryPanels: LibraryElementDTO[];
totalCount: number;
perPage: number;
page: number;
@@ -4,14 +4,14 @@ import { AsyncSelect, Button, Modal, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, SelectableValue, urlUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { DashboardSearchHit } from '../../../search/types';
import { getConnectedDashboards, getLibraryPanelConnectedDashboards } from '../../state/api';
import { debounce } from 'lodash';
export interface OpenLibraryPanelModalProps {
onDismiss: () => void;
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
}
export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPanelModalProps): JSX.Element {
@@ -47,8 +47,11 @@ export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPa
{connected > 0 ? (
<>
<p>
This panel is being used in <strong>{connected} dashboards</strong>.Please choose which dashboard to view
the panel in:
This panel is being used in{' '}
<strong>
{connected} {connected > 1 ? 'dashboards' : 'dashboard'}
</strong>
.Please choose which dashboard to view the panel in:
</p>
<AsyncSelect
isClearable
@@ -8,7 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state';
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { toPanelModelLibraryPanel } from '../../utils';
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@@ -23,7 +23,7 @@ interface Props {
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
const styles = useStyles2(getStyles);
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
const [changeToPanel, setChangeToPanel] = useState<LibraryPanelDTO | undefined>(undefined);
const [changeToPanel, setChangeToPanel] = useState<LibraryElementDTO | undefined>(undefined);
const [panelFilter, setPanelFilter] = useState<string[]>([]);
const onPanelFilterChange = useCallback(
(plugins: PanelPluginMeta[]) => {
@@ -63,7 +63,7 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
setShowingAddPanelModal(true);
};
const onChangeLibraryPanel = (panel: LibraryPanelDTO) => {
const onChangeLibraryPanel = (panel: LibraryElementDTO) => {
setChangeToPanel(panel);
};
+33 -18
View File
@@ -1,4 +1,10 @@
import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
import {
LibraryElementConnectionDTO,
LibraryElementDTO,
LibraryElementKind,
LibraryElementsSearchResult,
PanelModelWithLibraryPanel,
} from '../types';
import { DashboardSearchHit } from '../../search/types';
import { getBackendSrv } from '../../../core/services/backend_srv';
@@ -8,7 +14,7 @@ export interface GetLibraryPanelsOptions {
page?: number;
excludeUid?: string;
sortDirection?: string;
panelFilter?: string[];
typeFilter?: string[];
folderFilter?: string[];
}
@@ -18,35 +24,39 @@ export async function getLibraryPanels({
page = 1,
excludeUid = '',
sortDirection = '',
panelFilter = [],
typeFilter = [],
folderFilter = [],
}: GetLibraryPanelsOptions = {}): Promise<LibraryPanelSearchResult> {
}: GetLibraryPanelsOptions = {}): Promise<LibraryElementsSearchResult> {
const params = new URLSearchParams();
params.append('searchString', searchString);
params.append('sortDirection', sortDirection);
params.append('panelFilter', panelFilter.join(','));
params.append('typeFilter', typeFilter.join(','));
params.append('folderFilter', folderFilter.join(','));
params.append('excludeUid', excludeUid);
params.append('perPage', perPage.toString(10));
params.append('page', page.toString(10));
params.append('kind', LibraryElementKind.Panel.toString(10));
const { result } = await getBackendSrv().get(`/api/library-panels?${params.toString()}`);
const { result } = await getBackendSrv().get<{ result: LibraryElementsSearchResult }>(
`/api/library-elements?${params.toString()}`
);
return result;
}
export async function getLibraryPanel(uid: string): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().get(`/api/library-panels/${uid}`);
export async function getLibraryPanel(uid: string): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().get(`/api/library-elements/${uid}`);
return result;
}
export async function addLibraryPanel(
panelSaveModel: PanelModelWithLibraryPanel,
folderId: number
): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().post(`/api/library-panels`, {
): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().post(`/api/library-elements`, {
folderId,
name: panelSaveModel.title,
model: panelSaveModel,
kind: LibraryElementKind.Panel,
});
return result;
}
@@ -54,31 +64,36 @@ export async function addLibraryPanel(
export async function updateLibraryPanel(
panelSaveModel: PanelModelWithLibraryPanel,
folderId: number
): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().patch(`/api/library-panels/${panelSaveModel.libraryPanel.uid}`, {
): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().patch(`/api/library-elements/${panelSaveModel.libraryPanel.uid}`, {
folderId,
name: panelSaveModel.title,
model: panelSaveModel,
version: panelSaveModel.libraryPanel.version,
kind: LibraryElementKind.Panel,
});
return result;
}
export function deleteLibraryPanel(uid: string): Promise<{ message: string }> {
return getBackendSrv().delete(`/api/library-panels/${uid}`);
return getBackendSrv().delete(`/api/library-elements/${uid}`);
}
export async function getLibraryPanelConnectedDashboards(libraryPanelUid: string): Promise<number[]> {
const { result } = await getBackendSrv().get(`/api/library-panels/${libraryPanelUid}/dashboards`);
export async function getLibraryPanelConnectedDashboards(
libraryPanelUid: string
): Promise<LibraryElementConnectionDTO[]> {
const { result } = await getBackendSrv().get<{ result: LibraryElementConnectionDTO[] }>(
`/api/library-elements/${libraryPanelUid}/connections`
);
return result;
}
export async function getConnectedDashboards(uid: string): Promise<DashboardSearchHit[]> {
const dashboardIds = await getLibraryPanelConnectedDashboards(uid);
if (dashboardIds.length === 0) {
const connections = await getLibraryPanelConnectedDashboards(uid);
if (connections.length === 0) {
return [];
}
const searchHits = await getBackendSrv().search({ dashboardIds });
const searchHits = await getBackendSrv().search({ dashboardIds: connections.map((c) => c.connectionId) });
return searchHits;
}
+28 -10
View File
@@ -2,44 +2,62 @@ import { PanelModel } from '../dashboard/state';
import { Dispatch } from 'react';
import { AnyAction } from '@reduxjs/toolkit';
export interface LibraryPanelSearchResult {
export enum LibraryElementKind {
Panel = 1,
Variable,
}
export enum LibraryElementConnectionKind {
Dashboard = 1,
}
export interface LibraryElementConnectionDTO {
id: number;
kind: LibraryElementConnectionKind;
elementId: number;
connectionId: number;
created: string;
createdBy: LibraryElementDTOMetaUser;
}
export interface LibraryElementsSearchResult {
totalCount: number;
libraryPanels: LibraryPanelDTO[];
elements: LibraryElementDTO[];
perPage: number;
page: number;
}
export interface LibraryPanelDTO {
export interface LibraryElementDTO {
id: number;
orgId: number;
folderId: number;
uid: string;
name: string;
kind: LibraryElementKind;
type: string;
description: string;
model: any;
version: number;
meta: LibraryPanelDTOMeta;
meta: LibraryElementDTOMeta;
}
export interface LibraryPanelDTOMeta {
canEdit: boolean;
export interface LibraryElementDTOMeta {
folderName: string;
folderUid: string;
connectedDashboards: number;
created: string;
updated: string;
createdBy: LibraryPanelDTOMetaUser;
updatedBy: LibraryPanelDTOMetaUser;
createdBy: LibraryElementDTOMetaUser;
updatedBy: LibraryElementDTOMetaUser;
}
export interface LibraryPanelDTOMetaUser {
export interface LibraryElementDTOMetaUser {
id: number;
name: string;
avatarUrl: string;
}
export type PanelModelLibraryPanel = Pick<LibraryPanelDTO, 'uid' | 'name' | 'meta' | 'version'>;
export type PanelModelLibraryPanel = Pick<LibraryElementDTO, 'uid' | 'name' | 'meta' | 'version'>;
export interface PanelModelWithLibraryPanel extends PanelModel {
libraryPanel: PanelModelLibraryPanel;
+5 -5
View File
@@ -1,4 +1,4 @@
import { LibraryPanelDTO, PanelModelLibraryPanel } from './types';
import { LibraryElementDTO, PanelModelLibraryPanel } from './types';
import { PanelModel } from '../dashboard/state';
import { addLibraryPanel, updateLibraryPanel } from './state/api';
import { createErrorNotification, createSuccessNotification } from '../../core/copy/appNotification';
@@ -12,12 +12,12 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif
return createSuccessNotification(message);
}
export function toPanelModelLibraryPanel(libraryPanelDto: LibraryPanelDTO): PanelModelLibraryPanel {
export function toPanelModelLibraryPanel(libraryPanelDto: LibraryElementDTO): PanelModelLibraryPanel {
const { uid, name, meta, version } = libraryPanelDto;
return { uid, name, meta, version };
}
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryPanelDTO> {
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryElementDTO> {
const panelSaveModel = toPanelSaveModel(panel);
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId);
updatePanelModelWithUpdate(panel, savedPanel);
@@ -37,7 +37,7 @@ function toPanelSaveModel(panel: PanelModel): any {
return panelSaveModel;
}
function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO): void {
function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDTO): void {
panel.restoreModel({
...updated.model,
configRev: 0, // reset config rev, since changes have been saved
@@ -46,7 +46,7 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO)
panel.refresh();
}
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryPanelDTO> {
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryElementDTO> {
if (!panel.libraryPanel) {
return Promise.reject();
}