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

feat: auto setting active linked enterprise #1153

Merged
merged 1 commit into from
Jan 19, 2024

Conversation

alex-sheehan-edx
Copy link
Contributor

https://2u-internal.atlassian.net/browse/ENT-8038

For all changes

  • Ensure adequate tests are in place (or reviewed existing tests cover changes)

Only if submitting a visual change

  • Ensure to attach screenshots
  • Ensure to have UX team confirm screenshots

@alex-sheehan-edx alex-sheehan-edx force-pushed the asheehan-edx/ENT-8038 branch 2 times, most recently from 66066a3 to 395ae17 Compare January 9, 2024 18:34
Copy link

codecov bot commented Jan 9, 2024

Codecov Report

Attention: 5 lines in your changes are missing coverage. Please review.

Comparison is base (cc98d50) 85.33% compared to head (ff2820a) 85.34%.

❗ Current head ff2820a differs from pull request most recent head 80ebfb1. Consider uploading reports for the commit 80ebfb1 to get more accurate results

Files Patch % Lines
src/components/App/index.jsx 0.00% 3 Missing ⚠️
src/data/services/LmsApiService.js 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1153      +/-   ##
==========================================
+ Coverage   85.33%   85.34%   +0.01%     
==========================================
  Files         495      496       +1     
  Lines       10652    10694      +42     
  Branches     2232     2239       +7     
==========================================
+ Hits         9090     9127      +37     
- Misses       1520     1525       +5     
  Partials       42       42              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@alex-sheehan-edx alex-sheehan-edx force-pushed the asheehan-edx/ENT-8038 branch 2 times, most recently from ec355cf to 70423df Compare January 10, 2024 19:09
@alex-sheehan-edx alex-sheehan-edx requested a review from a team January 11, 2024 18:17
}) => {
const [isLoading, setIsLoading] = useState(false);

const updateActiveEnterpriseAndRefreshJWT = useCallback(async (username, currentEnterpriseId) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: likely don't necessarily need to pass currentEnterpriseId. This hook function itself gets enterpriseId as an argument, and it'd be available within updateActiveEnterpriseAndRefreshJWT without it as an explicit function argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize useCallback's get access to arguments passed to the hook!

enterpriseId,
user,
}) => {
const [isLoading, setIsLoading] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[curious, non-blocking] Might consider what scope is involved in refactoring this hook a bit to utilize @tanstack/react-query? What benefits might we get from React Query in relation to the scope? If the benefits are not significant in this particular use case, OK to not utilize React Query here.

E.g., removes boilerplate around needing to manage our own loading states, etc.

await LmsApiService.getActiveLinkedEnterprise(username).then(async (linkedEnterpriseId) => {
if (linkedEnterpriseId !== currentEnterpriseId) {
await LmsApiService.updateUserActiveEnterprise(currentEnterpriseId);
await LmsApiService.loginRefresh();
Copy link
Member

@adamstankiewicz adamstankiewicz Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[curious] I'm wondering if we need the loginRefresh call here anymore now that we're no longer making assumptions of JWT role ordering. That is, I believe we were calling loginRefresh because our prior assumption that the JWT roles would be re-ordered.

Given that this hook no longer cares about the JWT roles, could we nix the call to login_refresh?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, that's a great point! It's all done properly now 😄

src/data/services/LmsApiService.js Show resolved Hide resolved
src/data/services/LmsApiService.js Show resolved Hide resolved
}

static getActiveLinkedEnterprise(username) {
return this.fetchEnterpriseLearnerData({ username }).then((response) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might recommend using async/await over .then chaining.

return this.fetchEnterpriseLearnerData({ username }).then((response) => {
const { data } = response;
const results = data?.results;
for (let i = 0; i < results.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] might be able to trim/simplify this logic down a bit by relying on .find(...) instead of explicitly looping through each record to find the active=true?

static async getActiveLinkedEnterprise(username) {
    const response = await this.fetchEnterpriseLearnerData({ username });
    const transformedResponse = camelCaseObject(response.data);
    const enterprisesForLearner = transformedResponse.results;
    const activeLinkedEnterprise = enterprisesForLearner.find(enterprise => enterprise.active);
    if (!activeLinkedEnterprise) {
      logError(`${username} does not have any active linked enterprise customers`);
    }
}

static getActiveLinkedEnterprise(username) {
return this.fetchEnterpriseLearnerData({ username }).then((response) => {
const { data } = response;
const results = data?.results;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[curious] When would data be undefined? Do we need to use the optional chaining you have here? If we do, when data is undefined, results would also be undefined and would subsequently throw a JS error on line 431 when trying to access results.length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was mostly just following the pattern in the file- but I agree I can't think of a reason why it would be undefined.

return results[i].uuid;
}
}
return '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no active enterprise customer (super edge case, one should always exist), likely should return undefined or null instead of empty string.

@alex-sheehan-edx alex-sheehan-edx force-pushed the asheehan-edx/ENT-8038 branch 5 times, most recently from 4f11d11 to 6e1fe54 Compare January 17, 2024 19:25
Comment on lines 24 to 39
return {
isLoading,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: generally a good idea to return everything from useQuery's output in useUpdateActiveEnterpriseForUser (as opposed to cherry-picking just isLoading). That said, if you also adopt useMutation here per the above suggestion (non-blocking), then this comment isn't as relevant (since there'd be 2 loading states, 2 error states, etc.).

},
});

if (error) { logError(`Could not set active enterprise for learner, failed with error: ${logError}`); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend using the onError callback function passed to useQuery for any error logging as opposed to handling your own conditional here.

const useUpdateActiveEnterpriseForUser = ({ enterpriseId, user }) => {
const { username } = user;
const { isLoading, error } = useQuery({
queryKey: ['updateUsersActiveEnterprise'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Treat queryKey like dependency arrays. Any data that is utilized by queryFn should likely be part of the query key.

For example, if this query is specific to the given username, that username should be part of the query key. Rationale: the API call for a specific username is stored in the query cache based on the queryKey; without the username in the query key, useQuery will think it has the API data still cached even if that username has since changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense!

const useUpdateActiveEnterpriseForUser = ({ enterpriseId, user }) => {
const { username } = user;
const { isLoading, error } = useQuery({
queryKey: ['updateUsersActiveEnterprise'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider creating a query key factory similar to learnerCreditManagementQueryKeys. See https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories.

That said, totally fine to defer on this feedback for if/when we take on the work to migrate existing API calls to useQuery where query keys will become more important across the application.

const { isLoading, error } = useQuery({
queryKey: ['updateUsersActiveEnterprise'],
queryFn: async () => {
await LmsApiService.getActiveLinkedEnterprise(username).then(async (linkedEnterprise) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically, you wouldn't use .then chaining when using async and await, e.g.:

const activeLinkedEnterprise = await LmsApiService.getActiveLinkedEnterprise(username);
if (activeLinkedEnterprise.uuid !== enterpriseId) { ... }

queryFn: async () => {
await LmsApiService.getActiveLinkedEnterprise(username).then(async (linkedEnterprise) => {
if (linkedEnterprise.uuid !== enterpriseId) {
await LmsApiService.updateUserActiveEnterprise(enterpriseId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Generally, POST API calls are done with useMutation; useQuery is intended for reading data, not updating. While what you have should work, you might consider making use of useQuery for the GET request and useMutation for the POST request.

Main rationale is to avoid side effects that change server data within a useQuery (kind of similar to avoiding side effects in a GET API endpoint's implementation).

Something along the lines of the following (consider it pseudocode):

const useUpdateActiveEnterpriseForUser = ({ enterpriseId, user }) => {
  // Sets up POST call to update active enterprise.
  const { mutateAsync, isLoading: isUpdatingActiveEnterprise } = useMutation({
    mutationFn: (enterpriseId) => await LmsApiService.updateUserActiveEnterprise(enterpriseId);
    onError: (error) => {
      logError(`Could not set active enterprise for username ${username}, failed with error: ${error}`);
    },
  });

  // Determines active enterprise customer for authenticated user
  const { username } = user;
  const {
    data: activeEnterpriseCustomer,
    isLoading: isLoadingActiveEnterprise,
    ...restActiveLinkedCustomer
  } = useQuery({
    queryKey: ['enterprise', 'activeLinkedEnterpriseCustomer', username],
    queryFn: async () => await LmsApiService.getActiveLinkedEnterprise(username),
    onSuccess: (activeLinkedEnterprise) => {
      // Successfully retrieved learners active linked enterprise; if it differs from the viewed enterprise, make the viewed enterprise active.
      if (activeLinkedEnterprise.uuid !== enterpriseId) {
        await mutateAsync(enterpriseId);
      }
    },
    onError: (error) => {
      logError(`Could not fetch active enterprises for username ${username}, failed with error: ${error}`);
    },
  });

  return {
    isLoading: isLoadingActiveEnterprise || isUpdatingActiveEnterprise,
  };

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh I see, on successfully querying the active linked org, we use mutateAsync.. ok ok

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might have to tweak with the deprecation of the callbacks

@alex-sheehan-edx alex-sheehan-edx force-pushed the asheehan-edx/ENT-8038 branch 6 times, most recently from a2faa79 to ff2820a Compare January 19, 2024 15:58
@alex-sheehan-edx alex-sheehan-edx merged commit a2dcdc7 into master Jan 19, 2024
4 checks passed
@alex-sheehan-edx alex-sheehan-edx deleted the asheehan-edx/ENT-8038 branch January 19, 2024 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants