responding to comments

This commit is contained in:
Gabe Lyons 2025-06-25 15:51:48 -07:00
parent 8e6d2e0daa
commit 58e9604844
8 changed files with 155 additions and 35 deletions

View File

@ -101,6 +101,7 @@ public class MeResolver implements DataFetcher<CompletableFuture<AuthenticatedUs
AuthorizationUtils.canViewStructuredPropertiesPage(context));
platformPrivileges.setManageApplications(
ApplicationAuthorizationUtils.canManageApplications(context));
platformPrivileges.setManageFeatures(AuthorizationUtils.canManageFeatures(context));
// Construct and return authenticated user object.
final AuthenticatedUser authUser = new AuthenticatedUser();
authUser.setCorpUser(corpUser);

View File

@ -45,7 +45,7 @@ public class UpdateApplicationsSettingsResolver implements DataFetcher<Completab
final com.linkedin.settings.global.ApplicationsSettings newApplicationsSettings =
newGlobalSettings.hasApplications()
? newGlobalSettings.getApplications()
: new com.linkedin.settings.global.ApplicationsSettings().setEnabled(true);
: new com.linkedin.settings.global.ApplicationsSettings().setEnabled(false);
// Next, patch the actions settings.
updateApplicationsSettings(newApplicationsSettings, input);

View File

@ -182,6 +182,11 @@ type PlatformPrivileges {
Whether the user can manage applications.
"""
manageApplications: Boolean!
"""
Whether the user can manage platform features.
"""
manageFeatures: Boolean!
}
"""

View File

@ -0,0 +1,113 @@
package com.linkedin.datahub.graphql.resolvers.settings.applications;
import static com.linkedin.datahub.graphql.TestUtils.*;
import static org.mockito.ArgumentMatchers.any;
import static org.testng.Assert.*;
import com.linkedin.data.template.SetMode;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.UpdateApplicationsSettingsInput;
import com.linkedin.metadata.service.SettingsService;
import com.linkedin.settings.global.ApplicationsSettings;
import com.linkedin.settings.global.GlobalSettingsInfo;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletionException;
import org.mockito.Mockito;
import org.testng.annotations.Test;
public class UpdateApplicationsSettingsResolverTest {
private static final UpdateApplicationsSettingsInput TEST_INPUT_ENABLED =
new UpdateApplicationsSettingsInput(true);
private static final UpdateApplicationsSettingsInput TEST_INPUT_DISABLED =
new UpdateApplicationsSettingsInput(false);
@Test
public void testGetSuccessNoExistingSettings() throws Exception {
SettingsService mockService = initSettingsService(null);
UpdateApplicationsSettingsResolver resolver =
new UpdateApplicationsSettingsResolver(mockService);
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_ENABLED);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertTrue(resolver.get(mockEnv).get());
Mockito.verify(mockService, Mockito.times(1))
.updateGlobalSettings(
any(),
Mockito.eq(
new GlobalSettingsInfo()
.setApplications(new ApplicationsSettings().setEnabled(true))));
}
@Test
public void testGetSuccessExistingSettings() throws Exception {
SettingsService mockService = initSettingsService(new ApplicationsSettings().setEnabled(false));
UpdateApplicationsSettingsResolver resolver =
new UpdateApplicationsSettingsResolver(mockService);
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_ENABLED);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertTrue(resolver.get(mockEnv).get());
Mockito.verify(mockService, Mockito.times(1))
.updateGlobalSettings(
any(),
Mockito.eq(
new GlobalSettingsInfo()
.setApplications(new ApplicationsSettings().setEnabled(true))));
}
@Test
public void testGetSuccessDisableApplications() throws Exception {
SettingsService mockService = initSettingsService(new ApplicationsSettings().setEnabled(true));
UpdateApplicationsSettingsResolver resolver =
new UpdateApplicationsSettingsResolver(mockService);
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_DISABLED);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertTrue(resolver.get(mockEnv).get());
Mockito.verify(mockService, Mockito.times(1))
.updateGlobalSettings(
any(),
Mockito.eq(
new GlobalSettingsInfo()
.setApplications(new ApplicationsSettings().setEnabled(false))));
}
@Test
public void testGetUnauthorized() throws Exception {
SettingsService mockService = initSettingsService(new ApplicationsSettings().setEnabled(false));
UpdateApplicationsSettingsResolver resolver =
new UpdateApplicationsSettingsResolver(mockService);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
QueryContext mockContext = getMockDenyContext();
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_ENABLED);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
}
private static SettingsService initSettingsService(
ApplicationsSettings existingApplicationsSettings) {
SettingsService mockService = Mockito.mock(SettingsService.class);
Mockito.when(mockService.getGlobalSettings(any()))
.thenReturn(
new GlobalSettingsInfo()
.setApplications(existingApplicationsSettings, SetMode.IGNORE_NULL));
return mockService;
}
}

View File

@ -1,8 +1,10 @@
import { NetworkStatus } from '@apollo/client';
import { Modal, Table } from '@components';
import { Table } from '@components';
import { message } from 'antd';
import React, { useCallback, useMemo, useState } from 'react';
import { ConfirmationModal } from '@app/sharedV2/modals/ConfirmationModal';
import {
ApplicationActionsColumn,
ApplicationDescriptionColumn,
@ -97,12 +99,20 @@ const ApplicationsTable = ({ searchQuery, searchData, loading: propLoading, netw
refetch(); // Refresh the application list
})
.catch((e: any) => {
message.error(`Failed to delete tag: ${e.message}`);
message.error(`Failed to delete application: ${e.message}`);
});
setShowDeleteModal(false);
setApplicationUrnToDelete('');
setApplicationDisplayName('');
}, [deleteApplicationMutation, refetch, applicationUrnToDelete, applicationDisplayName]);
const handleDeleteClose = useCallback(() => {
setShowDeleteModal(false);
setApplicationUrnToDelete('');
setApplicationDisplayName('');
}, []);
const columns = useMemo(
() => [
{
@ -190,29 +200,15 @@ const ApplicationsTable = ({ searchQuery, searchData, loading: propLoading, netw
onChange={handleTableChange as any}
/>
{/* Delete confirmation modal - simplified */}
<Modal
title={`Delete application ${applicationDisplayName}`}
onCancel={() => setShowDeleteModal(false)}
open={showDeleteModal}
centered
buttons={[
{
text: 'Cancel',
color: 'violet',
variant: 'text',
onClick: () => setShowDeleteModal(false),
},
{
text: 'Delete',
color: 'red',
variant: 'filled',
onClick: handleDeleteApplication,
},
]}
>
<p>Are you sure you want to delete this application? This action cannot be undone.</p>
</Modal>
<ConfirmationModal
isOpen={showDeleteModal}
handleClose={handleDeleteClose}
handleConfirm={handleDeleteApplication}
modalTitle="Delete Application"
modalText={`Are you sure you want to delete the application "${applicationDisplayName}"? This action cannot be undone.`}
closeButtonText="Cancel"
confirmButtonText="Delete"
/>
</>
);
};

View File

@ -1,4 +1,5 @@
import { Button, PageTitle, Pagination, SearchBar, StructuredPopover } from '@components';
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
@ -8,6 +9,7 @@ import EmptyApplications from '@app/applications/EmptyApplications';
import { useUserContext } from '@app/context/useUserContext';
import { PageContainer } from '@app/govern/structuredProperties/styledComponents';
import { Message } from '@src/app/shared/Message';
import { DEBOUNCE_SEARCH_MS } from '@src/app/shared/constants';
import { useShowNavBarRedesign } from '@src/app/useShowNavBarRedesign';
import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated';
import { EntityType } from '@src/types.generated';
@ -62,13 +64,15 @@ const ManageApplications = () => {
const canManageApplications = userContext?.platformPrivileges?.manageApplications;
// Debounce search query input to reduce unnecessary renders
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 300);
const debouncedSetSearchQuery = useMemo(
() => debounce((query: string) => setDebouncedSearchQuery(query), DEBOUNCE_SEARCH_MS),
[],
);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
debouncedSetSearchQuery(searchQuery);
return () => debouncedSetSearchQuery.cancel();
}, [searchQuery, debouncedSetSearchQuery]);
// Search query configuration
const searchInputs = useMemo(

View File

@ -80,13 +80,13 @@ export const Preferences = () => {
const showSimplifiedHomepage = !!user?.settings?.appearance?.showSimplifiedHomepage;
const canManageApplications = userContext?.platformPrivileges?.manageApplications;
const applicationsEnabled = appConfig.config?.visualConfig?.application?.showApplicationInNavigation ?? false;
const [updateUserSettingMutation] = useUpdateUserSettingMutation();
const [updateApplicationsSettingsMutation] = useUpdateApplicationsSettingsMutation();
const showSimplifiedHomepageSetting = !isThemeV2;
const canManageApplicationAppearance = userContext?.platformPrivileges?.manageApplications;
return (
<Page>
@ -167,7 +167,7 @@ export const Preferences = () => {
</StyledCard>
</>
)}
{canManageApplications && (
{canManageApplicationAppearance && (
<StyledCard>
<UserSettingRow>
<TextContainer>
@ -195,7 +195,7 @@ export const Preferences = () => {
</UserSettingRow>
</StyledCard>
)}
{!showSimplifiedHomepageSetting && !isThemeV2Toggleable && !canManageApplications && (
{!showSimplifiedHomepageSetting && !isThemeV2Toggleable && !canManageApplicationAppearance && (
<div style={{ color: colors.gray[1700] }}>No appearance settings found.</div>
)}
</SourceContainer>

View File

@ -62,6 +62,7 @@ query getMe {
manageStructuredProperties
viewStructuredPropertiesPage
manageApplications
manageFeatures
}
}
}