mirror of
https://github.com/grafana/grafana.git
synced 2026-01-15 05:35:41 +00:00
Compare commits
2 Commits
gabor/no-p
...
restore-da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470ac231bf | ||
|
|
dffae66fdc |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1275,6 +1275,7 @@ embed.go @grafana/grafana-as-code
|
||||
/.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform
|
||||
/.github/workflows/i18n-crowdin-create-tasks.yml @grafana/grafana-frontend-platform
|
||||
/.github/workflows/i18n-verify.yml @grafana/grafana-frontend-platform
|
||||
/.github/workflows/deploy-storybook.yml @grafana/grafana-frontend-platform
|
||||
/.github/workflows/deploy-storybook-preview.yml @grafana/grafana-frontend-platform
|
||||
/.github/workflows/scripts/crowdin/create-tasks.ts @grafana/grafana-frontend-platform
|
||||
/.github/workflows/scripts/publish-frontend-metrics.mts @grafana/grafana-frontend-platform
|
||||
|
||||
79
.github/workflows/deploy-storybook.yml
vendored
Normal file
79
.github/workflows/deploy-storybook.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Deploy Storybook
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
# Only run in grafana/grafana
|
||||
if: github.repository == 'grafana/grafana'
|
||||
name: Detect whether code changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
changed-frontend-packages: ${{ steps.detect-changes.outputs.frontend-packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: true # required to get more history in the changed-files action
|
||||
fetch-depth: 2
|
||||
- name: Detect changes
|
||||
id: detect-changes
|
||||
uses: ./.github/actions/change-detection
|
||||
with:
|
||||
self: .github/workflows/deploy-storybook.yml
|
||||
deploy-storybook:
|
||||
name: Deploy Storybook
|
||||
runs-on: ubuntu-latest
|
||||
needs: detect-changes
|
||||
# Only run in grafana/grafana
|
||||
if: github.repository == 'grafana/grafana' && needs.detect-changes.outputs.changed-frontend-packages == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
BUCKET_NAME: grafana-storybook
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
|
||||
# Create the GCS folder name
|
||||
# Right now, this just returns "canary"
|
||||
# But we'll expand this to work for "latest" as well in the future
|
||||
- name: Create deploy name
|
||||
id: create-deploy-name
|
||||
run: |
|
||||
echo "deploy-name=canary" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Storybook
|
||||
uses: grafana/shared-workflows/actions/push-to-gcs@main
|
||||
with:
|
||||
environment: prod
|
||||
bucket: ${{ env.BUCKET_NAME }}
|
||||
bucket_path: ${{ steps.create-deploy-name.outputs.deploy-name }}
|
||||
path: packages/grafana-ui/dist/storybook
|
||||
service_account: github-gf-storybook-deploy@grafanalabs-workload-identity.iam.gserviceaccount.com
|
||||
parent: false
|
||||
36
public/app/features/browse-dashboards/api/recentlyViewed.ts
Normal file
36
public/app/features/browse-dashboards/api/recentlyViewed.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
import { DashboardQueryResult } from 'app/features/search/service/types';
|
||||
|
||||
/**
|
||||
* Returns dashboard search results ordered the same way the user opened them.
|
||||
*/
|
||||
export async function getRecentlyViewedDashboards(maxItems = 5): Promise<DashboardQueryResult[]> {
|
||||
try {
|
||||
const recentlyOpened = (await impressionSrv.getDashboardOpened()).slice(0, maxItems);
|
||||
if (!recentlyOpened.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResults = await getGrafanaSearcher().search({
|
||||
kind: ['dashboard'],
|
||||
limit: recentlyOpened.length,
|
||||
uid: recentlyOpened,
|
||||
});
|
||||
|
||||
const dashboards = searchResults.view.toArray();
|
||||
// Keep dashboards in the same order the user opened them.
|
||||
// When a UID is missing from the search response
|
||||
// push it to the end instead of letting indexOf return -1
|
||||
const order = (uid: string) => {
|
||||
const idx = recentlyOpened.indexOf(uid);
|
||||
return idx === -1 ? recentlyOpened.length : idx;
|
||||
};
|
||||
|
||||
dashboards.sort((a, b) => order(a.uid) - order(b.uid));
|
||||
return dashboards;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recently viewed dashboards', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { queryResultToViewItem } from 'app/features/search/service/utils';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { getFolderURL, isSharedWithMe } from '../components/utils';
|
||||
import { getFolderURL, isSharedWithMe } from '../utils/dashboards';
|
||||
|
||||
export const PAGE_SIZE = 50;
|
||||
|
||||
|
||||
@@ -13,20 +13,20 @@ import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import { canSelectItems } from '../permissions';
|
||||
import { fetchNextChildrenPage } from '../state/actions';
|
||||
import {
|
||||
useFlatTreeState,
|
||||
rootItemsSelector,
|
||||
useBrowseLoadingStatus,
|
||||
useCheckboxSelectionState,
|
||||
useChildrenByParentUIDState,
|
||||
useBrowseLoadingStatus,
|
||||
useFlatTreeState,
|
||||
useLoadNextChildrenPage,
|
||||
rootItemsSelector,
|
||||
} from '../state/hooks';
|
||||
import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice';
|
||||
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState, BrowseDashboardsPermissions } from '../types';
|
||||
import { setAllSelection, setFolderOpenState, setItemSelectionState } from '../state/slice';
|
||||
import { BrowseDashboardsPermissions, BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';
|
||||
|
||||
import { DashboardsTree } from './DashboardsTree';
|
||||
import { canSelectItems } from './utils';
|
||||
|
||||
interface BrowseViewProps {
|
||||
height: number;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { useSelectionRepoValidation } from 'app/features/provisioning/hooks/useS
|
||||
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { canEditItemType } from '../permissions';
|
||||
import { DashboardsTreeCellProps, SelectionState } from '../types';
|
||||
|
||||
import { isSharedWithMe, canEditItemType } from './utils';
|
||||
import { isSharedWithMe } from '../utils/dashboards';
|
||||
|
||||
export default function CheckboxCell({
|
||||
row: { original: row },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
||||
import { TableInstance, useTable } from 'react-table';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
@@ -11,20 +11,21 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
import { canSelectItems } from '../permissions';
|
||||
import {
|
||||
BrowseDashboardsPermissions,
|
||||
DashboardsTreeCellProps,
|
||||
DashboardsTreeColumn,
|
||||
DashboardsTreeItem,
|
||||
SelectionState,
|
||||
BrowseDashboardsPermissions,
|
||||
} from '../types';
|
||||
import { makeRowID } from '../utils/dashboards';
|
||||
|
||||
import CheckboxCell from './CheckboxCell';
|
||||
import CheckboxHeaderCell from './CheckboxHeaderCell';
|
||||
import { NameCell } from './NameCell';
|
||||
import { TagsCell } from './TagsCell';
|
||||
import { useCustomFlexLayout } from './customFlexTableLayout';
|
||||
import { makeRowID, canSelectItems } from './utils';
|
||||
|
||||
interface DashboardsTreeProps {
|
||||
items: DashboardsTreeItem[];
|
||||
|
||||
@@ -4,7 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui';
|
||||
import { Icon, IconButton, Link, Spinner, Text, useStyles2 } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/internal';
|
||||
import { getIconForItem } from 'app/features/search/service/utils';
|
||||
|
||||
@@ -12,8 +12,7 @@ import { Indent } from '../../../core/components/Indent/Indent';
|
||||
import { FolderRepo } from '../../../core/components/NestedFolderPicker/FolderRepo';
|
||||
import { useChildrenByParentUIDState } from '../state/hooks';
|
||||
import { DashboardsTreeCellProps } from '../types';
|
||||
|
||||
import { makeRowID } from './utils';
|
||||
import { makeRowID } from '../utils/dashboards';
|
||||
|
||||
const CHEVRON_SIZE = 'md';
|
||||
const ICON_SIZE = 'sm';
|
||||
|
||||
@@ -6,12 +6,12 @@ import { GrafanaTheme2, store } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
|
||||
import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui';
|
||||
import { Button, CollapsableSection, Grid, Spinner, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
|
||||
import { DashListItem } from 'app/plugins/panel/dashlist/DashListItem';
|
||||
|
||||
import { getRecentlyViewedDashboards } from './utils';
|
||||
import { getRecentlyViewedDashboards } from '../api/recentlyViewed';
|
||||
|
||||
const MAX_RECENT = 5;
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ import { SearchStateManager } from 'app/features/search/state/SearchStateManager
|
||||
import { DashboardViewItemKind, SearchState } from 'app/features/search/types';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { canEditItemType, canSelectItems } from '../permissions';
|
||||
import { useHasSelection } from '../state/hooks';
|
||||
import { setAllSelection, setItemSelectionState } from '../state/slice';
|
||||
import { BrowseDashboardsPermissions } from '../types';
|
||||
|
||||
import { canEditItemType, canSelectItems } from './utils';
|
||||
|
||||
interface SearchViewProps {
|
||||
height: number;
|
||||
width: number;
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { ResourceRef } from 'app/features/provisioning/components/BulkActions/useBulkActionJob';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
import { DashboardQueryResult } from 'app/features/search/service/types';
|
||||
|
||||
import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types';
|
||||
|
||||
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
||||
return baseId + item.uid;
|
||||
}
|
||||
|
||||
export function isSharedWithMe(uid: string) {
|
||||
return uid === config.sharedWithMeFolderUID;
|
||||
}
|
||||
|
||||
// Construct folder URL and append orgId to it
|
||||
export function getFolderURL(uid: string) {
|
||||
const { orgId } = contextSrv.user;
|
||||
const subUrlPrefix = config.appSubUrl ?? '';
|
||||
const url = `${subUrlPrefix}/dashboards/f/${uid}/`;
|
||||
|
||||
if (orgId) {
|
||||
return `${url}?orgId=${orgId}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Collect selected dashboard and folder from the DashboardTreeSelection
|
||||
// This is used to prepare the items for bulk delete operation.
|
||||
export function collectSelectedItems(selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>) {
|
||||
const resources: ResourceRef[] = [];
|
||||
|
||||
// folders
|
||||
for (const [uid, selected] of Object.entries(selectedItems.folder)) {
|
||||
if (selected) {
|
||||
resources.push({ name: uid, group: 'folder.grafana.app', kind: 'Folder' });
|
||||
}
|
||||
}
|
||||
|
||||
// dashboards
|
||||
for (const [uid, selected] of Object.entries(selectedItems.dashboard)) {
|
||||
if (selected) {
|
||||
resources.push({ name: uid, group: 'dashboard.grafana.app', kind: 'Dashboard' });
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
export function canEditItemType(itemKind: string, permissions: BrowseDashboardsPermissions) {
|
||||
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
|
||||
return itemKind === 'folder'
|
||||
? Boolean(canEditFolders || canDeleteFolders)
|
||||
: Boolean(canEditDashboards || canDeleteDashboards);
|
||||
}
|
||||
|
||||
export function canSelectItems(permissions: BrowseDashboardsPermissions) {
|
||||
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
|
||||
// Users can select items only if they have both edit and delete permissions for at least one item type
|
||||
const canSelectFolders = canEditFolders || canDeleteFolders;
|
||||
const canSelectDashboards = canEditDashboards || canDeleteDashboards;
|
||||
return Boolean(canSelectFolders || canSelectDashboards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dashboard search results ordered the same way the user opened them.
|
||||
*/
|
||||
export async function getRecentlyViewedDashboards(maxItems = 5): Promise<DashboardQueryResult[]> {
|
||||
try {
|
||||
const recentlyOpened = (await impressionSrv.getDashboardOpened()).slice(0, maxItems);
|
||||
if (!recentlyOpened.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResults = await getGrafanaSearcher().search({
|
||||
kind: ['dashboard'],
|
||||
limit: recentlyOpened.length,
|
||||
uid: recentlyOpened,
|
||||
});
|
||||
|
||||
const dashboards = searchResults.view.toArray();
|
||||
// Keep dashboards in the same order the user opened them.
|
||||
// When a UID is missing from the search response
|
||||
// push it to the end instead of letting indexOf return -1
|
||||
const order = (uid: string) => {
|
||||
const idx = recentlyOpened.indexOf(uid);
|
||||
return idx === -1 ? recentlyOpened.length : idx;
|
||||
};
|
||||
|
||||
dashboards.sort((a, b) => order(a.uid) - order(b.uid));
|
||||
return dashboards;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recently viewed dashboards', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
import { FolderDTO } from 'app/types/folders';
|
||||
|
||||
import { BrowseDashboardsPermissions } from './types';
|
||||
|
||||
function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDTO) {
|
||||
return folderDTO ? contextSrv.hasPermissionInMetadata(action, folderDTO) : contextSrv.hasPermission(action);
|
||||
}
|
||||
@@ -31,3 +33,18 @@ export function getFolderPermissions(folderDTO?: FolderDTO) {
|
||||
canDeleteDashboards,
|
||||
};
|
||||
}
|
||||
|
||||
export function canEditItemType(itemKind: string, permissions: BrowseDashboardsPermissions) {
|
||||
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
|
||||
return itemKind === 'folder'
|
||||
? Boolean(canEditFolders || canDeleteFolders)
|
||||
: Boolean(canEditDashboards || canDeleteDashboards);
|
||||
}
|
||||
|
||||
export function canSelectItems(permissions: BrowseDashboardsPermissions) {
|
||||
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
|
||||
// Users can select items only if they have both edit and delete permissions for at least one item type
|
||||
const canSelectFolders = canEditFolders || canDeleteFolders;
|
||||
const canSelectDashboards = canEditDashboards || canDeleteDashboards;
|
||||
return Boolean(canSelectFolders || canSelectDashboards);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useCallback, useRef } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useSelector, StoreState, useDispatch } from 'app/types/store';
|
||||
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import { isSharedWithMe } from '../components/utils';
|
||||
import {
|
||||
BrowseDashboardsState,
|
||||
DashboardsTreeItem,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
DashboardViewItemWithUIItems,
|
||||
UIDashboardViewItem,
|
||||
} from '../types';
|
||||
import { isSharedWithMe } from '../utils/dashboards';
|
||||
|
||||
import { fetchNextChildrenPage } from './actions';
|
||||
import { getPaginationPlaceholders } from './utils';
|
||||
|
||||
@@ -3,8 +3,8 @@ import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
|
||||
|
||||
import { GENERAL_FOLDER_UID } from '../../search/constants';
|
||||
import { isSharedWithMe } from '../components/utils';
|
||||
import { BrowseDashboardsState } from '../types';
|
||||
import { isSharedWithMe } from '../utils/dashboards';
|
||||
|
||||
import { fetchNextChildrenPage, refetchChildren } from './actions';
|
||||
import { findItem } from './utils';
|
||||
|
||||
47
public/app/features/browse-dashboards/utils/dashboards.ts
Normal file
47
public/app/features/browse-dashboards/utils/dashboards.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { ResourceRef } from 'app/features/provisioning/components/BulkActions/useBulkActionJob';
|
||||
|
||||
import { DashboardTreeSelection, DashboardViewItemWithUIItems } from '../types';
|
||||
|
||||
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
||||
return baseId + item.uid;
|
||||
}
|
||||
|
||||
export function isSharedWithMe(uid: string) {
|
||||
return uid === config.sharedWithMeFolderUID;
|
||||
}
|
||||
|
||||
// Construct folder URL and append orgId to it
|
||||
export function getFolderURL(uid: string) {
|
||||
const { orgId } = contextSrv.user;
|
||||
const subUrlPrefix = config.appSubUrl ?? '';
|
||||
const url = `${subUrlPrefix}/dashboards/f/${uid}/`;
|
||||
|
||||
if (orgId) {
|
||||
return `${url}?orgId=${orgId}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Collect selected dashboard and folder from the DashboardTreeSelection
|
||||
// This is used to prepare the items for bulk delete operation.
|
||||
export function collectSelectedItems(selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>) {
|
||||
const resources: ResourceRef[] = [];
|
||||
|
||||
// folders
|
||||
for (const [uid, selected] of Object.entries(selectedItems.folder)) {
|
||||
if (selected) {
|
||||
resources.push({ name: uid, group: 'folder.grafana.app', kind: 'Folder' });
|
||||
}
|
||||
}
|
||||
|
||||
// dashboards
|
||||
for (const [uid, selected] of Object.entries(selectedItems.dashboard)) {
|
||||
if (selected) {
|
||||
resources.push({ name: uid, group: 'dashboard.grafana.app', kind: 'Dashboard' });
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
Reference in New Issue
Block a user