Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID-1357 Use Sam for Starred Workspaces #5004

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ const mockTerraUserProfile = {
programLocationState: 'testProgramLocationState',
programLocationCountry: 'testProgramLocationCountry',
researchArea: 'testResearchArea',
starredWorkspaces: 'testStarredWorkspaces',
};

const testSamUserAllowancesDetails = {
Expand All @@ -79,6 +78,7 @@ const mockSamUserCombinedState: SamUserCombinedStateResponse = {
terraUserAttributes: { marketingConsent: false },
termsOfService: mockSamUserTermsOfServiceDetails,
enterpriseFeatures: [],
favoriteResources: [],
};

const mockNihDatasetPermission = {
Expand Down
3 changes: 2 additions & 1 deletion src/auth/user-profile/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const loadTerraUser = async (): Promise<void> => {
const getProfile = User().profile.get();
const getCombinedState = User().getSamUserCombinedState();
const [profile, terraUserCombinedState] = await Promise.all([getProfile, getCombinedState]);
const { terraUserAttributes, enterpriseFeatures, samUser, terraUserAllowances, termsOfService } =
const { terraUserAttributes, enterpriseFeatures, samUser, terraUserAllowances, termsOfService, favoriteResources } =
terraUserCombinedState;
clearNotification(sessionExpirationProps.id);
userStore.update((state: TerraUserState) => ({
Expand All @@ -27,6 +27,7 @@ export const loadTerraUser = async (): Promise<void> => {
terraUserAttributes,
enterpriseFeatures,
samUser,
favoriteResources,
}));
authStore.update((state: AuthState) => ({
...state,
Expand Down
1 change: 0 additions & 1 deletion src/libs/ajax/User.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const completeUserProfile: TerraUserProfile = {
programLocationCity: 'testCity',
programLocationCountry: 'testCountry',
programLocationState: 'testState',
starredWorkspaces: 'testStarredWorkspaces',
title: 'testTitle',
researchArea: 'testResearchArea',
} as const satisfies TerraUserProfile;
Expand Down
36 changes: 34 additions & 2 deletions src/libs/ajax/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { jsonBody } from '@terra-ui-packages/data-client-core';
import _ from 'lodash/fp';
import { authOpts } from 'src/auth/auth-fetch';
import { fetchOrchestration, fetchSam } from 'src/libs/ajax/ajax-common';
import { FullyQualifiedResourceId } from 'src/libs/ajax/SamResources';
import { SamUserTermsOfServiceDetails } from 'src/libs/ajax/TermsOfService';
import { TerraUserProfile } from 'src/libs/state';

Expand All @@ -22,8 +23,6 @@ export interface SamUserAllowances {
}

export type TerraUserPreferences = {
starredWorkspaces?: string;
} & {
// These are the key value pairs from the workspace notification settings in the form of:
// 'notifications/SuccessfulSubmissionNotification/${workspace.workspace.namespace}/${workspace.workspace.name}' : true
// TODO for a follow-up ticket:
Expand Down Expand Up @@ -171,6 +170,7 @@ export type SamUserCombinedStateResponse = {
terraUserAttributes: SamUserAttributes;
termsOfService: SamUserTermsOfServiceDetails;
enterpriseFeatures: string[];
favoriteResources: FullyQualifiedResourceId[];
};

export type OrchestrationUserRegistrationRequest = object;
Expand Down Expand Up @@ -229,12 +229,15 @@ export const User = (signal?: AbortSignal) => {
? responseJson.additionalDetails.enterpriseFeatures.resources.map((resource) => resource.resourceId)
: [];

const favoriteResources = responseJson.favoriteResources;

return {
samUser,
terraUserAllowances,
terraUserAttributes,
termsOfService,
enterpriseFeatures,
favoriteResources,
};
},

Expand Down Expand Up @@ -291,6 +294,35 @@ export const User = (signal?: AbortSignal) => {
},
},

favorites: {
get: async (): Promise<FullyQualifiedResourceId[]> => {
const res = await fetchSam('api/users/v2/self/favoriteResources', _.merge(authOpts(), { signal }));
return res.json();
},

getResourceType: async (resourceTypeName: string): Promise<FullyQualifiedResourceId[]> => {
const res = await fetchSam(
`api/users/v2/self/favoriteResources/${resourceTypeName}`,
_.merge(authOpts(), { signal })
);
return res.json();
},

put: async (resource: FullyQualifiedResourceId): Promise<void> => {
await fetchSam(
`api/users/v2/self/favoriteResources/${resource.resourceTypeName}/${resource.resourceId}`,
_.merge(authOpts(), { signal, method: 'PUT' })
);
},

delete: async (resource: FullyQualifiedResourceId): Promise<void> => {
await fetchSam(
`api/users/v2/self/favoriteResources/${resource.resourceTypeName}/${resource.resourceId}`,
_.merge(authOpts(), { signal, method: 'DELETE' })
);
},
},

// Returns the proxy group email of the user with the given email
getProxyGroup: async (email: string): Promise<string> => {
const res = await fetchOrchestration(
Expand Down
7 changes: 4 additions & 3 deletions src/libs/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OidcUser } from 'src/auth/oidc-broker';
import { Dataset } from 'src/libs/ajax/Catalog';
import { EcmLinkAccountResponse } from 'src/libs/ajax/ExternalCredentials';
import { OidcConfig } from 'src/libs/ajax/OAuth2';
import { FullyQualifiedResourceId } from 'src/libs/ajax/SamResources';
import { SamTermsOfServiceConfig } from 'src/libs/ajax/TermsOfService';
import { NihDatasetPermission, SamUserAllowances, SamUserAttributes, SamUserResponse } from 'src/libs/ajax/User';
import { getLocalStorage, getSessionStorage, staticStorageSlot } from 'src/libs/browser-storage';
Expand Down Expand Up @@ -109,7 +110,7 @@ export interface TerraUser {
export interface TerraUserProfile {
// TODO: anonymousGroup is here from getProfile from orch
// TODO: for future ticket, separate items updated via register/profile (personal info)
// from things that are updated via api/profile/preferences (starred workspaces, notification settings)
// from things that are updated via api/profile/preferences (notification settings)
firstName: string | undefined;
lastName: string | undefined;
institute: string | undefined;
Expand All @@ -121,7 +122,6 @@ export interface TerraUserProfile {
programLocationState?: string;
programLocationCountry?: string;
researchArea?: string;
starredWorkspaces?: string;
}

export interface TerraUserState {
Expand All @@ -130,6 +130,7 @@ export interface TerraUserState {
terraUserAttributes: SamUserAttributes;
enterpriseFeatures: string[];
samUser: SamUserResponse;
favoriteResources: FullyQualifiedResourceId[];
}

/**
Expand All @@ -147,7 +148,6 @@ export const userStore: Atom<TerraUserState> = atom<TerraUserState>({
programLocationState: undefined,
programLocationCountry: undefined,
interestInTerra: undefined,
starredWorkspaces: undefined,
},
terraUser: {
token: undefined,
Expand All @@ -174,6 +174,7 @@ export const userStore: Atom<TerraUserState> = atom<TerraUserState>({
registeredAt: undefined,
updatedAt: undefined,
},
favoriteResources: [],
});

export const getTerraUser = (): TerraUser => userStore.get().terraUser;
Expand Down
2 changes: 0 additions & 2 deletions src/profile/Profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ describe('Profile', () => {

researchArea: '',
interestInTerra: '',

starredWorkspaces: undefined,
};

it('loads the user profile', () => {
Expand Down
2 changes: 0 additions & 2 deletions src/profile/personal-info/PersonalInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ describe('PersonalInfo', () => {

researchArea: '',
interestInTerra: '',

starredWorkspaces: undefined,
};

it('renders the initial profile', () => {
Expand Down
6 changes: 2 additions & 4 deletions src/profile/useUserProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ const mockProfile: TerraUserProfile = {
programLocationCountry: '',

researchArea: '',
interestInTerra: '',

starredWorkspaces: undefined,
interestInTerra: undefined,
};

describe('useUserProfile', () => {
Expand Down Expand Up @@ -192,7 +190,7 @@ describe('useUserProfile', () => {

// Assert
// Not all profile fields are updated via this request.
expect(updateProfile).toHaveBeenCalledWith(_.omit(['email', 'starredWorkspaces'], updatedProfile));
expect(updateProfile).toHaveBeenCalledWith(_.omit(['email', 'interestInTerra'], updatedProfile));
});

it('returns loading status while profile is updating', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const setupMockAjax = async (
terraUserAttributes: { marketingConsent: false },
termsOfService,
enterpriseFeatures: [],
favoriteResources: [],
};
const getTermsOfServiceText = jest.fn().mockResolvedValue('some text');
const getUserTermsOfServiceDetails = jest
Expand Down
7 changes: 3 additions & 4 deletions src/workspaces/list/RenderedWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
canRead,
getCloudProviderFromWorkspace,
isGoogleWorkspace,
starredWorkspacesFromFavoriteResources,
workspaceAccessLevels,
WorkspaceInfo,
WorkspacePolicy,
Expand Down Expand Up @@ -105,10 +106,8 @@ const getColumns = (

export const RenderedWorkspaces = (props: RenderedWorkspacesProps): ReactNode => {
const { workspaces } = props;
const {
profile: { starredWorkspaces },
} = useStore<TerraUserState>(userStore);
const starredWorkspaceIds = _.isEmpty(starredWorkspaces) ? [] : _.split(',', starredWorkspaces);
const { favoriteResources } = useStore<TerraUserState>(userStore);
const starredWorkspaceIds = starredWorkspacesFromFavoriteResources(favoriteResources);

const [sort, setSort] = useState<WorkspaceSort>({ field: 'lastModified', direction: 'desc' });

Expand Down
138 changes: 138 additions & 0 deletions src/workspaces/list/WorkspaceStarControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { expect } from '@storybook/test';
import { DeepPartial } from '@terra-ui-packages/core-utils';
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { User } from 'src/libs/ajax/User';
import { userStore } from 'src/libs/state';
import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils';
import { defaultInitializedGoogleWorkspace } from 'src/testing/workspace-fixtures';
import { WorkspaceResourceTypeName } from 'src/workspaces/utils';

import { WorkspaceStarControl } from './WorkspaceStarControl';

type UserExports = typeof import('src/libs/ajax/User');
jest.mock('src/libs/ajax/User', (): UserExports => {
return {
...jest.requireActual<UserExports>('src/libs/ajax/User'),
User: jest.fn(),
};
});
type UserContract = ReturnType<typeof User>;

const favoriteResources = [
{
resourceTypeName: WorkspaceResourceTypeName,
resourceId: defaultInitializedGoogleWorkspace.workspace.workspaceId,
},
];

describe('WorkspaceStarControl', () => {
describe('When a user has been loaded', () => {
describe('and the workspace is not starred', () => {
it('renders an "unchecked" star', async () => {
// Arrange
userStore.update((state) => {
return { ...state, favoriteResources: [] };
});
// Act
await render(<WorkspaceStarControl workspace={defaultInitializedGoogleWorkspace} />);

// Assert
const star = screen.getByRole('checkbox');
expect(star).not.toBeChecked();
});
});
describe('and the workspace is starred', () => {
it('renders a "checked" star', async () => {
// Arrange
userStore.update((state) => {
return { ...state, favoriteResources };
});

// Act
await render(<WorkspaceStarControl workspace={defaultInitializedGoogleWorkspace} />);

// Assert
const star = screen.getByRole('checkbox');
expect(star).toBeChecked();
});
});
});
describe('when a user stars a workspace', () => {
it('tells sam to favorite the workspace and renders a "checked" star', async () => {
// Arrange
const user = userEvent.setup();

userStore.update((state) => {
return { ...state, favoriteResources: [] };
});

const getFavoriteResourcesFunction = jest.fn().mockResolvedValue(favoriteResources);
const putFavoriteResourcesFunction = jest.fn().mockResolvedValue({});
const deleteFavoriteResourcesFunction = jest.fn().mockResolvedValue({});
asMockedFn(User).mockImplementation(() => {
return {
favorites: {
get: getFavoriteResourcesFunction,
put: putFavoriteResourcesFunction,
delete: deleteFavoriteResourcesFunction,
},
} as DeepPartial<UserContract> as UserContract;
});
// Act
await act(() => render(<WorkspaceStarControl workspace={defaultInitializedGoogleWorkspace} />));

// Assert
const star = screen.getByRole('checkbox');
expect(star).not.toBeChecked();

await user.click(star);

expect(getFavoriteResourcesFunction).toHaveBeenCalled();
expect(putFavoriteResourcesFunction).toHaveBeenCalled();
expect(deleteFavoriteResourcesFunction).not.toHaveBeenCalled();

const newStar = screen.getByRole('checkbox');
expect(newStar).toBeChecked;
});
});
describe('when a user un-stars a workspace', () => {
it('tells sam to un-favorite the workspace and renders an "unchecked" star', async () => {
// Arrange
const user = userEvent.setup();

userStore.update((state) => {
return { ...state, favoriteResources };
});

const getFavoriteResourcesFunction = jest.fn().mockResolvedValue([]);
const putFavoriteResourcesFunction = jest.fn().mockResolvedValue({});
const deleteFavoriteResourcesFunction = jest.fn().mockResolvedValue({});
asMockedFn(User).mockImplementation(() => {
return {
favorites: {
get: getFavoriteResourcesFunction,
put: putFavoriteResourcesFunction,
delete: deleteFavoriteResourcesFunction,
},
} as DeepPartial<UserContract> as UserContract;
});
// Act
await act(() => render(<WorkspaceStarControl workspace={defaultInitializedGoogleWorkspace} />));

// Assert
const star = screen.getByRole('checkbox');
expect(star).toBeChecked();

await user.click(star);

expect(getFavoriteResourcesFunction).toHaveBeenCalled();
expect(putFavoriteResourcesFunction).not.toHaveBeenCalled();
expect(deleteFavoriteResourcesFunction).toHaveBeenCalled();

const newStar = screen.getByRole('checkbox');
expect(newStar).not.toBeChecked;
});
});
});
Loading
Loading