mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Replace folderID with folderUID (#56414)
* user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/database.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 lastFile:pkg/services/libraryelements/writers.go * user essentials mob! 🔱 * support filterFolderUIDs in the frontend * move common logic to a variable * fixed FolderLibraryPanelsPage and improved unit test * fix backend lint error * fix formatting error Co-authored-by: Joao Silva <joao.silva@grafana.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: eledobleefe <laura.fernandez@grafana.com> Co-authored-by: joshhunt <josh@trtr.co>
This commit is contained in:
@@ -133,14 +133,15 @@ func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Respon
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Response {
|
||||
query := searchLibraryElementsQuery{
|
||||
perPage: c.QueryInt("perPage"),
|
||||
page: c.QueryInt("page"),
|
||||
searchString: c.Query("searchString"),
|
||||
sortDirection: c.Query("sortDirection"),
|
||||
kind: c.QueryInt("kind"),
|
||||
typeFilter: c.Query("typeFilter"),
|
||||
excludeUID: c.Query("excludeUid"),
|
||||
folderFilter: c.Query("folderFilter"),
|
||||
perPage: c.QueryInt("perPage"),
|
||||
page: c.QueryInt("page"),
|
||||
searchString: c.Query("searchString"),
|
||||
sortDirection: c.Query("sortDirection"),
|
||||
kind: c.QueryInt("kind"),
|
||||
typeFilter: c.Query("typeFilter"),
|
||||
excludeUID: c.Query("excludeUid"),
|
||||
folderFilter: c.Query("folderFilter"),
|
||||
folderFilterUIDs: c.Query("folderFilterUIDs"),
|
||||
}
|
||||
elementsResult, err := l.getAllLibraryElements(c.Req.Context(), c.SignedInUser, query)
|
||||
if err != nil {
|
||||
|
@@ -409,6 +409,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
||||
var libraryElements []LibraryElement
|
||||
countBuilder := db.SQLBuilder{}
|
||||
countBuilder.Write("SELECT * FROM library_element AS le")
|
||||
countBuilder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id")
|
||||
countBuilder.Write(` WHERE le.org_id=?`, signedInUser.OrgID)
|
||||
writeKindSQL(query, &countBuilder)
|
||||
writeSearchStringSQL(query, l.SQLStore, &countBuilder)
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -14,6 +15,10 @@ func isGeneralFolder(folderID int64) bool {
|
||||
return folderID == 0
|
||||
}
|
||||
|
||||
func isUIDGeneralFolder(folderUID string) bool {
|
||||
return folderUID == accesscontrol.GeneralFolderUID
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error {
|
||||
kind := models.LibraryElementKind(kindAsInt)
|
||||
switch kind {
|
||||
|
@@ -207,14 +207,15 @@ type PatchLibraryElementCommand struct {
|
||||
|
||||
// searchLibraryElementsQuery is the query used for searching for Elements
|
||||
type searchLibraryElementsQuery struct {
|
||||
perPage int
|
||||
page int
|
||||
searchString string
|
||||
sortDirection string
|
||||
kind int
|
||||
typeFilter string
|
||||
excludeUID string
|
||||
folderFilter string
|
||||
perPage int
|
||||
page int
|
||||
searchString string
|
||||
sortDirection string
|
||||
kind int
|
||||
typeFilter string
|
||||
excludeUID string
|
||||
folderFilter string
|
||||
folderFilterUIDs string
|
||||
}
|
||||
|
||||
// LibraryElementResponse is a response struct for LibraryElementDTO.
|
||||
|
@@ -2,6 +2,7 @@ package libraryelements
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -72,41 +73,58 @@ func writeExcludeSQL(query searchLibraryElementsQuery, builder *db.SQLBuilder) {
|
||||
type FolderFilter struct {
|
||||
includeGeneralFolder bool
|
||||
folderIDs []string
|
||||
folderUIDs []string
|
||||
parseError error
|
||||
}
|
||||
|
||||
func parseFolderFilter(query searchLibraryElementsQuery) FolderFilter {
|
||||
folderIDs := make([]string, 0)
|
||||
if len(strings.TrimSpace(query.folderFilter)) == 0 {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: true,
|
||||
folderIDs: folderIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
}
|
||||
folderUIDs := make([]string, 0)
|
||||
hasFolderFilter := len(strings.TrimSpace(query.folderFilter)) > 0
|
||||
hasFolderFilterUID := len(strings.TrimSpace(query.folderFilterUIDs)) > 0
|
||||
|
||||
includeGeneralFolder := false
|
||||
folderIDs = strings.Split(query.folderFilter, ",")
|
||||
for _, filter := range folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: false,
|
||||
folderIDs: folderIDs,
|
||||
parseError: err,
|
||||
}
|
||||
}
|
||||
if isGeneralFolder(folderID) {
|
||||
includeGeneralFolder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: includeGeneralFolder,
|
||||
result := FolderFilter{
|
||||
includeGeneralFolder: true,
|
||||
folderIDs: folderIDs,
|
||||
folderUIDs: folderUIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
|
||||
if hasFolderFilter && hasFolderFilterUID {
|
||||
result.parseError = errors.New("cannot pass both folderFilter and folderFilterUIDs")
|
||||
return result
|
||||
}
|
||||
|
||||
if hasFolderFilter {
|
||||
result.includeGeneralFolder = false
|
||||
folderIDs = strings.Split(query.folderFilter, ",")
|
||||
result.folderIDs = folderIDs
|
||||
for _, filter := range folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
result.parseError = err
|
||||
}
|
||||
if isGeneralFolder(folderID) {
|
||||
result.includeGeneralFolder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasFolderFilterUID {
|
||||
result.includeGeneralFolder = false
|
||||
folderUIDs = strings.Split(query.folderFilterUIDs, ",")
|
||||
result.folderUIDs = folderUIDs
|
||||
|
||||
for _, folderUID := range folderUIDs {
|
||||
if isUIDGeneralFolder(folderUID) {
|
||||
result.includeGeneralFolder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *db.SQLBuilder) error {
|
||||
@@ -127,5 +145,17 @@ func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *db.SQL
|
||||
builder.Write(sql.String(), params...)
|
||||
}
|
||||
|
||||
paramsUIDs := make([]interface{}, 0)
|
||||
for _, folderUID := range f.folderUIDs {
|
||||
if !includeGeneral && isUIDGeneralFolder(folderUID) {
|
||||
continue
|
||||
}
|
||||
paramsUIDs = append(paramsUIDs, folderUID)
|
||||
}
|
||||
if len(paramsUIDs) > 0 {
|
||||
sql.WriteString(` AND dashboard.uid IN (?` + strings.Repeat(",?", len(paramsUIDs)-1) + ")")
|
||||
builder.Write(sql.String(), paramsUIDs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -73,9 +73,9 @@ async function getFoldersAsOptions(
|
||||
|
||||
// FIXME: stop using id from search and use UID instead
|
||||
const searchHits: DashboardSearchHit[] = await getBackendSrv().search(params);
|
||||
const options = searchHits.map((d) => ({ label: d.title, value: { id: d.id, title: d.title } }));
|
||||
const options = searchHits.map((d) => ({ label: d.title, value: { uid: d.uid, title: d.title } }));
|
||||
if (!searchString || 'general'.includes(searchString.toLowerCase())) {
|
||||
options.unshift({ label: 'General', value: { id: 0, title: 'General' } });
|
||||
options.unshift({ label: 'General', value: { uid: 'general', title: 'General' } });
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
@@ -21,7 +21,6 @@ const mapStateToProps = (state: StoreState, props: OwnProps) => {
|
||||
return {
|
||||
pageNav: getNavModel(state.navIndex, `folder-library-panels-${uid}`, getLoadingNav(1)),
|
||||
folderUid: uid,
|
||||
folder: state.folder,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +32,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export function FolderLibraryPanelsPage({ pageNav, getFolderByUid, folderUid, folder }: Props): JSX.Element {
|
||||
export function FolderLibraryPanelsPage({ pageNav, getFolderByUid, folderUid }: Props): JSX.Element {
|
||||
const { loading } = useAsync(async () => await getFolderByUid(folderUid), [getFolderByUid, folderUid]);
|
||||
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
|
||||
|
||||
@@ -42,7 +41,7 @@ export function FolderLibraryPanelsPage({ pageNav, getFolderByUid, folderUid, fo
|
||||
<Page.Contents isLoading={loading}>
|
||||
<LibraryPanelsSearch
|
||||
onClick={setSelected}
|
||||
currentFolderId={folder.id}
|
||||
currentFolderUID={folderUid}
|
||||
showSecondaryActions
|
||||
showSort
|
||||
showPanelFilter
|
||||
|
@@ -100,7 +100,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
await waitFor(() =>
|
||||
expect(getLibraryPanelsSpy).toHaveBeenCalledWith({
|
||||
searchString: 'a',
|
||||
folderFilter: [],
|
||||
folderFilterUIDs: [],
|
||||
page: 0,
|
||||
typeFilter: [],
|
||||
perPage: 40,
|
||||
@@ -128,7 +128,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
expect(getLibraryPanelsSpy).toHaveBeenCalledWith({
|
||||
searchString: '',
|
||||
sortDirection: 'alpha-desc',
|
||||
folderFilter: [],
|
||||
folderFilterUIDs: [],
|
||||
page: 0,
|
||||
typeFilter: [],
|
||||
perPage: 40,
|
||||
@@ -156,7 +156,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
await waitFor(() =>
|
||||
expect(getLibraryPanelsSpy).toHaveBeenCalledWith({
|
||||
searchString: '',
|
||||
folderFilter: [],
|
||||
folderFilterUIDs: [],
|
||||
page: 0,
|
||||
typeFilter: ['graph', 'timeseries'],
|
||||
perPage: 40,
|
||||
@@ -177,21 +177,52 @@ describe('LibraryPanelsSearch', () => {
|
||||
|
||||
describe('and user changes folder filter', () => {
|
||||
it('should call api with correct params', async () => {
|
||||
const { getLibraryPanelsSpy } = await getTestContext({ showFolderFilter: true });
|
||||
const { getLibraryPanelsSpy } = await getTestContext(
|
||||
{ showFolderFilter: true, currentFolderUID: 'wXyZ1234' },
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Library Panel Name',
|
||||
kind: LibraryElementKind.Panel,
|
||||
uid: 'uid',
|
||||
description: 'Library Panel Description',
|
||||
folderId: 0,
|
||||
model: { type: 'timeseries', title: 'A title' },
|
||||
type: 'timeseries',
|
||||
orgId: 1,
|
||||
version: 1,
|
||||
meta: {
|
||||
folderName: 'General',
|
||||
folderUid: '',
|
||||
connectedDashboards: 0,
|
||||
created: '2021-01-01 12:00:00',
|
||||
createdBy: { id: 1, name: 'Admin', avatarUrl: '' },
|
||||
updated: '2021-01-01 12:00:00',
|
||||
updatedBy: { id: 1, name: 'Admin', avatarUrl: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
perPage: 40,
|
||||
page: 1,
|
||||
totalCount: 0,
|
||||
}
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('combobox', { name: /folder filter/i }));
|
||||
await userEvent.type(screen.getByRole('combobox', { name: /folder filter/i }), '{enter}', {
|
||||
await userEvent.type(screen.getByRole('combobox', { name: /folder filter/i }), 'library', {
|
||||
skipClick: true,
|
||||
});
|
||||
await waitFor(() =>
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getLibraryPanelsSpy).toHaveBeenCalledWith({
|
||||
searchString: '',
|
||||
folderFilter: ['0'],
|
||||
folderFilterUIDs: ['wXyZ1234'],
|
||||
page: 0,
|
||||
typeFilter: [],
|
||||
perPage: 40,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -26,7 +26,7 @@ export interface LibraryPanelsSearchProps {
|
||||
showFolderFilter?: boolean;
|
||||
showSecondaryActions?: boolean;
|
||||
currentPanelId?: string;
|
||||
currentFolderId?: number;
|
||||
currentFolderUID?: string;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const LibraryPanelsSearch = ({
|
||||
onClick,
|
||||
variant = LibraryPanelsSearchVariant.Spacious,
|
||||
currentPanelId,
|
||||
currentFolderId,
|
||||
currentFolderUID,
|
||||
perPage = DEFAULT_PER_PAGE_PAGINATION,
|
||||
showPanelFilter = false,
|
||||
showFolderFilter = false,
|
||||
@@ -48,7 +48,7 @@ export const LibraryPanelsSearch = ({
|
||||
useDebounce(() => setDebouncedSearchQuery(searchQuery), 200, [searchQuery]);
|
||||
|
||||
const [sortDirection, setSortDirection] = useState<SelectableValue<string>>({});
|
||||
const [folderFilter, setFolderFilter] = useState<string[]>(currentFolderId ? [String(currentFolderId)] : []);
|
||||
const [folderFilter, setFolderFilter] = useState<string[]>(currentFolderUID ? [currentFolderUID] : []);
|
||||
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||
|
||||
const sortOrFiltersVisible = showSort || showPanelFilter || showFolderFilter;
|
||||
@@ -155,7 +155,7 @@ const SearchControls = React.memo(
|
||||
[onPanelFilterChange]
|
||||
);
|
||||
const folderFilterChanged = useCallback(
|
||||
(folders: FolderInfo[]) => onFolderFilterChange(folders.map((f) => String(f.id))),
|
||||
(folders: FolderInfo[]) => onFolderFilterChange(folders.map((f) => f.uid ?? '')),
|
||||
[onFolderFilterChange]
|
||||
);
|
||||
|
||||
|
@@ -51,7 +51,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
searchString,
|
||||
sortDirection,
|
||||
panelFilter,
|
||||
folderFilter,
|
||||
folderFilterUIDs: folderFilter,
|
||||
page,
|
||||
perPage,
|
||||
currentPanelId,
|
||||
|
@@ -14,7 +14,7 @@ interface SearchArgs {
|
||||
searchString: string;
|
||||
sortDirection?: string;
|
||||
panelFilter?: string[];
|
||||
folderFilter?: string[];
|
||||
folderFilterUIDs?: string[];
|
||||
currentPanelId?: string;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
|
||||
excludeUid: args.currentPanelId,
|
||||
sortDirection: args.sortDirection,
|
||||
typeFilter: args.panelFilter,
|
||||
folderFilter: args.folderFilter,
|
||||
folderFilterUIDs: args.folderFilterUIDs,
|
||||
})
|
||||
).pipe(
|
||||
mergeMap(({ perPage, elements: libraryPanels, page, totalCount }) =>
|
||||
|
@@ -19,7 +19,7 @@ export interface GetLibraryPanelsOptions {
|
||||
excludeUid?: string;
|
||||
sortDirection?: string;
|
||||
typeFilter?: string[];
|
||||
folderFilter?: string[];
|
||||
folderFilterUIDs?: string[];
|
||||
}
|
||||
|
||||
export async function getLibraryPanels({
|
||||
@@ -29,13 +29,13 @@ export async function getLibraryPanels({
|
||||
excludeUid = '',
|
||||
sortDirection = '',
|
||||
typeFilter = [],
|
||||
folderFilter = [],
|
||||
folderFilterUIDs = [],
|
||||
}: GetLibraryPanelsOptions = {}): Promise<LibraryElementsSearchResult> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('searchString', searchString);
|
||||
params.append('sortDirection', sortDirection);
|
||||
params.append('typeFilter', typeFilter.join(','));
|
||||
params.append('folderFilter', folderFilter.join(','));
|
||||
params.append('folderFilterUIDs', folderFilterUIDs.join(','));
|
||||
params.append('excludeUid', excludeUid);
|
||||
params.append('perPage', perPage.toString(10));
|
||||
params.append('page', page.toString(10));
|
||||
|
@@ -28,7 +28,11 @@ export interface FolderState {
|
||||
}
|
||||
|
||||
export interface FolderInfo {
|
||||
/**
|
||||
* @deprecated use uid instead.
|
||||
*/
|
||||
id?: number;
|
||||
uid?: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
canViewFolderPermissions?: boolean;
|
||||
|
Reference in New Issue
Block a user