diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 080bb2f1c1a..10f2f1bb313 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -38,9 +38,9 @@ func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([] resp := make([]PublicDashboardListResponse, 0) err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { - sess.Table("dashboard_public"). + sess.Table("dashboard_public").Select( + "dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title"). Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id"). - Cols("dashboard_public.uid", "dashboard_public.access_token", "dashboard_public.dashboard_uid", "dashboard_public.is_enabled", "dashboard.title"). Where("dashboard_public.org_id = ?", orgId). OrderBy(" is_enabled DESC, dashboard.title IS NULL, dashboard.title ASC") diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx index a6fbb91e907..254ea59467e 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx @@ -32,6 +32,23 @@ const publicDashboardListResponse: ListPublicDashboardResponse[] = [ }, ]; +const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [ + { + uid: 'SdZwuCZVz2', + accessToken: 'beeaf92f6ab3467f80b2be922c7741ab', + title: '', + dashboardUid: '', + isEnabled: false, + }, + { + uid: 'EuiEbd3nz2', + accessToken: '8687b0498ccf4babb2f92810d8563b33', + title: '', + dashboardUid: '', + isEnabled: true, + }, +]; + const server = setupServer( rest.get('/api/dashboards/public-dashboards', (_, res, ctx) => res(ctx.status(200), ctx.json(publicDashboardListResponse)) @@ -107,7 +124,11 @@ describe('Show table', () => { }); it('renders public dashboards in a good way without trashcan', async () => { jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); - await renderPublicDashboardItems(); + + await renderPublicDashboardTable(true); + publicDashboardListResponse.forEach((pd, idx) => { + renderPublicDashboardItemCorrectly(pd, idx, false); + }); const tableBody = screen.getAllByRole('rowgroup')[1]; const tableRows = within(tableBody).getAllByRole('row'); @@ -120,7 +141,11 @@ describe('Show table', () => { }); it('renders public dashboards in a good way with trashcan', async () => { jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); - await renderPublicDashboardItems(); + + await renderPublicDashboardTable(true); + publicDashboardListResponse.forEach((pd, idx) => { + renderPublicDashboardItemCorrectly(pd, idx, true); + }); const tableBody = screen.getAllByRole('rowgroup')[1]; const tableRows = within(tableBody).getAllByRole('row'); @@ -131,17 +156,25 @@ describe('Show table', () => { expect(within(rowDataCells[2]).getByTestId(selectors.ListItem.trashcanButton)); }); }); + it('renders public dashboards items correctly', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + await renderPublicDashboardTable(true); + + publicDashboardListResponse.forEach((pd, idx) => { + renderPublicDashboardItemCorrectly(pd, idx, true); + }); + }); }); describe('Delete public dashboard', () => { - it('when user does not have public dashboard write permissions, then dashboards are listed without delete button ', async () => { + it('when user does not have public dashboard write permissions, then dashboards are listed without delete button', async () => { jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); await renderPublicDashboardTable(true); const tableBody = screen.getAllByRole('rowgroup')[1]; expect(within(tableBody).queryAllByTestId(selectors.ListItem.trashcanButton)).toHaveLength(0); }); - it('when user has public dashboard write permissions, then dashboards are listed with delete button ', async () => { + it('when user has public dashboard write permissions, then dashboards are listed with delete button', async () => { jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); await renderPublicDashboardTable(true); @@ -152,20 +185,46 @@ describe('Delete public dashboard', () => { }); }); -const renderPublicDashboardItems = async () => { - await renderPublicDashboardTable(true); +describe('Orphaned public dashboard', () => { + it('renders orphaned and non orphaned public dashboards items correctly', async () => { + const response = [...publicDashboardListResponse, ...orphanedDashboardListResponse]; + server.use( + rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(response)); + }) + ); + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + + await renderPublicDashboardTable(true); + response.forEach((pd, idx) => { + renderPublicDashboardItemCorrectly(pd, idx, true); + }); + }); +}); + +const renderPublicDashboardItemCorrectly = (pd: ListPublicDashboardResponse, idx: number, hasWriteAccess: boolean) => { + const isOrphaned = !pd.dashboardUid; const tableBody = screen.getAllByRole('rowgroup')[1]; const tableRows = within(tableBody).getAllByRole('row'); - publicDashboardListResponse.forEach((pd, idx) => { - const tableRow = tableRows[idx]; - const rowDataCells = within(tableRow).getAllByRole('cell'); - expect(rowDataCells).toHaveLength(3); + const tableRow = tableRows[idx]; + const rowDataCells = within(tableRow).getAllByRole('cell'); + expect(rowDataCells).toHaveLength(3); - expect(within(rowDataCells[0]).getByText(pd.title)); - expect(within(rowDataCells[1]).getByText(pd.isEnabled ? 'enabled' : 'disabled')); - expect(within(rowDataCells[2]).getByTestId(selectors.ListItem.linkButton)); - expect(within(rowDataCells[2]).getByTestId(selectors.ListItem.configButton)); - }); + const statusTag = within(rowDataCells[1]).getByText(pd.isEnabled ? 'enabled' : 'disabled'); + const linkButton = within(rowDataCells[2]).getByTestId(selectors.ListItem.linkButton); + const configButton = within(rowDataCells[2]).getByTestId(selectors.ListItem.configButton); + const trashcanButton = within(rowDataCells[2]).queryByTestId(selectors.ListItem.trashcanButton); + + expect(within(rowDataCells[0]).getByText(isOrphaned ? 'Orphaned public dashboard' : pd.title)).toBeInTheDocument(); + expect(statusTag).toBeInTheDocument(); + isOrphaned ? expect(statusTag).toHaveStyle('background-color: rgb(110, 110, 110)') : expect(statusTag).toBeEnabled(); + isOrphaned || !pd.isEnabled + ? expect(linkButton).toHaveStyle('pointer-events: none') + : expect(linkButton).not.toHaveStyle('pointer-events: none'); + isOrphaned + ? expect(configButton).toHaveStyle('pointer-events: none') + : expect(configButton).not.toHaveStyle('pointer-events: none'); + hasWriteAccess ? expect(trashcanButton).toBeEnabled() : expect(trashcanButton).toBeNull(); }; diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx index 3470524a766..2b624e53a1d 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx @@ -12,6 +12,8 @@ import { useListPublicDashboardsQuery } from 'app/features/dashboard/api/publicD import { isOrgAdmin } from 'app/features/plugins/admin/permissions'; import { AccessControlAction } from 'app/types'; +import { ListPublicDashboardResponse } from '../../types'; + import { DeletePublicDashboardButton } from './DeletePublicDashboardButton'; export const viewPublicDashboardUrl = (accessToken: string): string => @@ -41,45 +43,64 @@ export const PublicDashboardListTable = () => { - {publicDashboards?.map((pd) => ( - - - - - {pd.title} - - - - - - - - - { + const isOrphaned = !pd.dashboardUid; + return ( + + + - - - - - - {hasWritePermissions && } - - - - ))} + {!isOrphaned ? ( + + {pd.title} + + ) : ( +
+

Orphaned public dashboard

+ +
+ )} + + + + + + + + + + + + + + {hasWritePermissions && ( + + )} + + + + ); + })} @@ -110,5 +131,19 @@ function getStyles(theme: GrafanaTheme2, isMobile: boolean) { buttonGroup: css` justify-content: ${isMobile ? 'space-between' : 'end'}; `, + orphanedTitle: css` + display: flex; + align-items: center; + + p { + margin: ${theme.spacing(0, 1, 0, 0)}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + `, + orphanedInfoIcon: css` + color: ${theme.colors.text.link}; + `, }; }