Skip to content

Commit df0147f

Browse files
ref(dashboards): Fixes and refactoring for edit access selector button (#80633)
#79833 Some fixes for the Edit Access dropdown button in Dashboards. 1. modified behaviour of `Save Changes` button in `EditAccessSelector.tsx` so that it's enabled only when the changed selections are not the same as the existing perms 2. pass down props so that Widget edit buttons are disabled when user does not have edit perms 3. pass down props to top level dashboard filters so that users without edit perms cannot save changes 4. pass down props to widget context menu to disable the `Edit Widget` button there <img width="500" alt="Screenshot 2024-11-13 at 11 04 49 AM" src="https://github.com/user-attachments/assets/7931fbb5-620d-46e7-bde5-b832b6978554"> <img width="500" alt="Screenshot 2024-11-14 at 4 12 23 PM" src="https://github.com/user-attachments/assets/f83c3d66-16d0-4e12-8184-a645bb4a2be7">
1 parent dbce322 commit df0147f

File tree

11 files changed

+180
-21
lines changed

11 files changed

+180
-21
lines changed

static/app/components/modals/widgetViewerModal.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {components} from 'react-select';
33
import {css} from '@emotion/react';
44
import styled from '@emotion/styled';
55
import * as Sentry from '@sentry/react';
6+
import type {User} from '@sentry/types';
67
import {truncate} from '@sentry/utils';
78
import type {DataZoomComponentOption} from 'echarts';
89
import type {Location} from 'history';
@@ -54,9 +55,16 @@ import useApi from 'sentry/utils/useApi';
5455
import {useLocation} from 'sentry/utils/useLocation';
5556
import {useNavigate} from 'sentry/utils/useNavigate';
5657
import useProjects from 'sentry/utils/useProjects';
58+
import {useUser} from 'sentry/utils/useUser';
59+
import {useUserTeams} from 'sentry/utils/useUserTeams';
5760
import withPageFilters from 'sentry/utils/withPageFilters';
61+
import {checkUserHasEditAccess} from 'sentry/views/dashboards/detail';
5862
import {DiscoverSplitAlert} from 'sentry/views/dashboards/discoverSplitAlert';
59-
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
63+
import type {
64+
DashboardFilters,
65+
DashboardPermissions,
66+
Widget,
67+
} from 'sentry/views/dashboards/types';
6068
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
6169
import {
6270
dashboardFiltersToString,
@@ -104,7 +112,9 @@ export interface WidgetViewerModalOptions {
104112
organization: Organization;
105113
widget: Widget;
106114
widgetLegendState: WidgetLegendSelectionState;
115+
dashboardCreator?: User;
107116
dashboardFilters?: DashboardFilters;
117+
dashboardPermissions?: DashboardPermissions;
108118
onEdit?: () => void;
109119
onMetricWidgetEdit?: (widget: Widget) => void;
110120
pageLinks?: string;
@@ -194,6 +204,8 @@ function WidgetViewerModal(props: Props) {
194204
seriesResultsType,
195205
dashboardFilters,
196206
widgetLegendState,
207+
dashboardPermissions,
208+
dashboardCreator,
197209
} = props;
198210
const location = useLocation();
199211
const {projects} = useProjects();
@@ -843,6 +855,18 @@ function WidgetViewerModal(props: Props) {
843855
}
844856
}
845857

858+
const currentUser = useUser();
859+
const {teams: userTeams} = useUserTeams();
860+
let hasEditAccess = true;
861+
if (organization.features.includes('dashboards-edit-access')) {
862+
hasEditAccess = checkUserHasEditAccess(
863+
currentUser,
864+
userTeams,
865+
organization,
866+
dashboardPermissions,
867+
dashboardCreator
868+
);
869+
}
846870
function renderWidgetViewer() {
847871
return (
848872
<Fragment>
@@ -1059,6 +1083,7 @@ function WidgetViewerModal(props: Props) {
10591083
display_type: widget.displayType,
10601084
});
10611085
}}
1086+
disabled={!hasEditAccess}
10621087
>
10631088
{t('Edit Widget')}
10641089
</Button>

static/app/views/dashboards/controls.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ function Controls({
150150
let hasEditAccess = true;
151151
if (organization.features.includes('dashboards-edit-access')) {
152152
hasEditAccess = checkUserHasEditAccess(
153-
dashboard,
154153
currentUser,
155154
userTeams,
156-
organization
155+
organization,
156+
dashboard.permissions,
157+
dashboard.createdBy
157158
);
158159
}
159160

static/app/views/dashboards/dashboard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ class Dashboard extends Component<Props, State> {
434434
<div key={key} data-grid={widget.layout}>
435435
<SortableWidget
436436
{...widgetProps}
437+
dashboardPermissions={dashboard.permissions}
438+
dashboardCreator={dashboard.createdBy}
437439
isMobile={isMobile}
438440
windowWidth={windowWidth}
439441
index={String(index)}

static/app/views/dashboards/detail.spec.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,80 @@ describe('Dashboards > Detail', function () {
19311931
expect(screen.getByRole('button', {name: 'Add Widget'})).toBeDisabled();
19321932
});
19331933

1934+
it('disables widget edit, duplicate, and delete button when user does not have edit perms', async function () {
1935+
const widget = {
1936+
displayType: types.DisplayType.TABLE,
1937+
interval: '1d',
1938+
queries: [
1939+
{
1940+
name: 'Test Widget',
1941+
fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
1942+
columns: ['project'],
1943+
aggregates: ['count()', 'count_unique(user)', 'epm()'],
1944+
conditions: '',
1945+
orderby: '',
1946+
},
1947+
],
1948+
title: 'Transactions',
1949+
id: '1',
1950+
widgetType: types.WidgetType.DISCOVER,
1951+
};
1952+
const mockDashboard = DashboardFixture([widget], {
1953+
id: '1',
1954+
title: 'Custom Errors',
1955+
createdBy: UserFixture({id: '238900'}),
1956+
permissions: {isEditableByEveryone: false},
1957+
});
1958+
MockApiClient.addMockResponse({
1959+
url: '/organizations/org-slug/dashboards/',
1960+
body: mockDashboard,
1961+
});
1962+
MockApiClient.addMockResponse({
1963+
url: '/organizations/org-slug/dashboards/1/',
1964+
body: mockDashboard,
1965+
});
1966+
1967+
const currentUser = UserFixture({id: '781629'});
1968+
ConfigStore.set('user', currentUser);
1969+
1970+
render(
1971+
<ViewEditDashboard
1972+
{...RouteComponentPropsFixture()}
1973+
organization={{
1974+
...initialData.organization,
1975+
features: ['dashboards-edit-access', ...initialData.organization.features],
1976+
}}
1977+
params={{orgId: 'org-slug', dashboardId: '1'}}
1978+
router={initialData.router}
1979+
location={initialData.router.location}
1980+
>
1981+
{null}
1982+
</ViewEditDashboard>,
1983+
{
1984+
router: initialData.router,
1985+
organization: {
1986+
features: ['dashboards-edit-access', ...initialData.organization.features],
1987+
},
1988+
}
1989+
);
1990+
1991+
await screen.findByText('Edit Access:');
1992+
expect(screen.getByRole('button', {name: 'Edit Dashboard'})).toBeDisabled();
1993+
expect(screen.getByRole('button', {name: 'Add Widget'})).toBeDisabled();
1994+
await userEvent.click(await screen.findByLabelText('Widget actions'));
1995+
expect(
1996+
screen.getByRole('menuitemradio', {name: 'Duplicate Widget'})
1997+
).toHaveAttribute('aria-disabled', 'true');
1998+
expect(screen.getByRole('menuitemradio', {name: 'Delete Widget'})).toHaveAttribute(
1999+
'aria-disabled',
2000+
'true'
2001+
);
2002+
expect(screen.getByRole('menuitemradio', {name: 'Edit Widget'})).toHaveAttribute(
2003+
'aria-disabled',
2004+
'true'
2005+
);
2006+
});
2007+
19342008
describe('discover split', function () {
19352009
it('calls the dashboard callbacks with the correct widgetType for discover split', function () {
19362010
const widget = {

static/app/views/dashboards/detail.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,23 @@ export function handleUpdateDashboardSplit({
172172

173173
/* Checks if current user has permissions to edit dashboard */
174174
export function checkUserHasEditAccess(
175-
dashboard: DashboardDetails,
176175
currentUser: User,
177176
userTeams: Team[],
178-
organization: Organization
177+
organization: Organization,
178+
dashboardPermissions?: DashboardPermissions,
179+
dashboardCreator?: User
179180
): boolean {
180181
if (
181182
!organization.features.includes('dashboards-edit-access') ||
182-
!dashboard.permissions ||
183-
dashboard.permissions.isEditableByEveryone ||
184-
dashboard.createdBy?.id === currentUser.id
183+
!dashboardPermissions ||
184+
dashboardPermissions.isEditableByEveryone ||
185+
dashboardCreator?.id === currentUser.id
185186
) {
186187
return true;
187188
}
188-
if (dashboard.permissions.teamsWithEditAccess?.length) {
189+
if (dashboardPermissions.teamsWithEditAccess?.length) {
189190
const userTeamIds = userTeams.map(team => Number(team.id));
190-
dashboard.permissions.teamsWithEditAccess.some(teamId =>
191-
userTeamIds.includes(teamId)
192-
);
191+
dashboardPermissions.teamsWithEditAccess.some(teamId => userTeamIds.includes(teamId));
193192
}
194193
return false;
195194
}
@@ -273,6 +272,8 @@ class DashboardDetail extends Component<Props, State> {
273272
totalIssuesCount,
274273
widgetLegendState: this.state.widgetLegendState,
275274
dashboardFilters: getDashboardFiltersFromURL(location) ?? dashboard.filters,
275+
dashboardPermissions: dashboard.permissions,
276+
dashboardCreator: dashboard.createdBy,
276277
onMetricWidgetEdit: (updatedWidget: Widget) => {
277278
const widgets = [...dashboard.widgets];
278279

@@ -845,6 +846,8 @@ class DashboardDetail extends Component<Props, State> {
845846
</StyledPageHeader>
846847
<HookHeader organization={organization} />
847848
<FiltersBar
849+
dashboardPermissions={dashboard.permissions}
850+
dashboardCreator={dashboard.createdBy}
848851
filters={{}} // Default Dashboards don't have filters set
849852
location={location}
850853
hasUnsavedChanges={false}
@@ -1018,6 +1021,8 @@ class DashboardDetail extends Component<Props, State> {
10181021
) : null}
10191022
<FiltersBar
10201023
filters={(modifiedDashboard ?? dashboard).filters}
1024+
dashboardPermissions={dashboard.permissions}
1025+
dashboardCreator={dashboard.createdBy}
10211026
location={location}
10221027
hasUnsavedChanges={hasUnsavedFilters}
10231028
isEditingDashboard={

static/app/views/dashboards/editAccessSelector.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ function EditAccessSelector({dashboard, onChangeEditAccess}: EditAccessSelectorP
3838
const teamIds: string[] = Object.values(teams).map(team => team.id);
3939
const [selectedOptions, setSelectedOptions] = useState<string[]>(getSelectedOptions());
4040
const [isMenuOpen, setMenuOpen] = useState<boolean>(false);
41-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
4241

4342
// Handles state change when dropdown options are selected
4443
const onSelectOptions = newSelectedOptions => {
@@ -176,7 +175,6 @@ function EditAccessSelector({dashboard, onChangeEditAccess}: EditAccessSelectorP
176175
setMenuOpen(false);
177176
if (isMenuOpen) {
178177
setSelectedOptions(getSelectedOptions());
179-
setHasUnsavedChanges(false);
180178
}
181179
}}
182180
disabled={!isCurrentUserDashboardOwner}
@@ -198,7 +196,10 @@ function EditAccessSelector({dashboard, onChangeEditAccess}: EditAccessSelectorP
198196
setMenuOpen(!isMenuOpen);
199197
}}
200198
priority="primary"
201-
disabled={!isCurrentUserDashboardOwner || !hasUnsavedChanges}
199+
disabled={
200+
!isCurrentUserDashboardOwner ||
201+
isEqual(getDashboardPermissions(), dashboard.permissions)
202+
}
202203
>
203204
{t('Save Changes')}
204205
</Button>
@@ -210,7 +211,6 @@ function EditAccessSelector({dashboard, onChangeEditAccess}: EditAccessSelectorP
210211
size="sm"
211212
onChange={newSelectedOptions => {
212213
onSelectOptions(newSelectedOptions);
213-
setHasUnsavedChanges(true);
214214
}}
215215
multiple
216216
searchable
@@ -223,7 +223,6 @@ function EditAccessSelector({dashboard, onChangeEditAccess}: EditAccessSelectorP
223223
setMenuOpen(!isMenuOpen);
224224
if (isMenuOpen) {
225225
setSelectedOptions(getSelectedOptions());
226-
setHasUnsavedChanges(false);
227226
}
228227
}}
229228
menuFooter={dropdownFooterButtons}

static/app/views/dashboards/filtersBar.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Fragment} from 'react';
22
import styled from '@emotion/styled';
3+
import type {User} from '@sentry/types';
34
import type {Location} from 'history';
45

56
import {Button} from 'sentry/components/button';
@@ -17,9 +18,12 @@ import {decodeList} from 'sentry/utils/queryString';
1718
import {ReleasesProvider} from 'sentry/utils/releases/releasesProvider';
1819
import useOrganization from 'sentry/utils/useOrganization';
1920
import usePageFilters from 'sentry/utils/usePageFilters';
21+
import {useUser} from 'sentry/utils/useUser';
22+
import {useUserTeams} from 'sentry/utils/useUserTeams';
23+
import {checkUserHasEditAccess} from 'sentry/views/dashboards/detail';
2024

2125
import ReleasesSelectControl from './releasesSelectControl';
22-
import type {DashboardFilters} from './types';
26+
import type {DashboardFilters, DashboardPermissions} from './types';
2327
import {DashboardFilterKeys} from './types';
2428

2529
type FiltersBarProps = {
@@ -29,12 +33,16 @@ type FiltersBarProps = {
2933
isPreview: boolean;
3034
location: Location;
3135
onDashboardFilterChange: (activeFilters: DashboardFilters) => void;
36+
dashboardCreator?: User;
37+
dashboardPermissions?: DashboardPermissions;
3238
onCancel?: () => void;
3339
onSave?: () => void;
3440
};
3541

3642
export default function FiltersBar({
3743
filters,
44+
dashboardPermissions,
45+
dashboardCreator,
3846
hasUnsavedChanges,
3947
isEditingDashboard,
4048
isPreview,
@@ -45,6 +53,18 @@ export default function FiltersBar({
4553
}: FiltersBarProps) {
4654
const {selection} = usePageFilters();
4755
const organization = useOrganization();
56+
const currentUser = useUser();
57+
const {teams: userTeams} = useUserTeams();
58+
let hasEditAccess = true;
59+
if (organization.features.includes('dashboards-edit-access')) {
60+
hasEditAccess = checkUserHasEditAccess(
61+
currentUser,
62+
userTeams,
63+
organization,
64+
dashboardPermissions,
65+
dashboardCreator
66+
);
67+
}
4868

4969
const selectedReleases =
5070
(defined(location.query?.[DashboardFilterKeys.RELEASE])
@@ -100,7 +120,7 @@ export default function FiltersBar({
100120
</FilterButtons>
101121
{hasUnsavedChanges && !isEditingDashboard && !isPreview && (
102122
<FilterButtons>
103-
<Button priority="primary" onClick={onSave}>
123+
<Button priority="primary" onClick={onSave} disabled={!hasEditAccess}>
104124
{t('Save')}
105125
</Button>
106126
<Button onClick={onCancel}>{t('Cancel')}</Button>

static/app/views/dashboards/metrics/widgetCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export function MetricWidgetCard({
208208
showContextMenu
209209
isPreview={false}
210210
widgetLimitReached={false}
211+
hasEditAccess
211212
onEdit={() => {
212213
router.push({
213214
pathname: `${location.pathname}${

0 commit comments

Comments
 (0)