Skip to content

Commit ae6e7bc

Browse files
committed
chore: user Preference Hook logic separated
1 parent fe93d6a commit ae6e7bc

File tree

10 files changed

+280
-172
lines changed

10 files changed

+280
-172
lines changed

src/Shared/Components/SelectPicker/SelectPicker.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
334334

335335
const renderLoadingMessage = () => {
336336
if (shouldShowLoadingMessage) {
337-
return <p className="m-0 cn-7 fs-13 fw-4 lh-20 py-6 px-8">Loading...</p>
337+
return <p className="m-0 cn-7 fs-13 fw-4 lh-20 py-6 px-8 dc__loading-dots">Loading</p>
338338
}
339339
return null
340340
}

src/Shared/Hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from './useGetResourceKindsOptions'
1919
export * from './UseDownload'
2020
export * from './useForm'
2121
export * from './useStickyEvent'
22+
export * from './useUserPreferences'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { useUserPreferences } from './useUserPrefrences'
2+
export * from './constants'
3+
export * from './types'
4+
export * from './service'
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { get, patch } from '@Common/Api'
2+
import { ROUTES } from '@Common/Constants'
3+
import { getUrlWithSearchParams, showError } from '@Common/index'
4+
import { ResourceKindType, BaseAppMetaData } from '@Shared/index'
5+
import { THEME_PREFERENCE_MAP, ThemeConfigType, ThemePreferenceType } from '@Shared/Providers/ThemeProvider/types'
6+
import { USER_PREFERENCES_ATTRIBUTE_KEY } from './constants'
7+
import {
8+
UserPreferencesType,
9+
GetUserPreferencesQueryParamsType,
10+
GetUserPreferencesParsedDTO,
11+
ViewIsPipelineRBACConfiguredRadioTabs,
12+
UserPreferenceResourceType,
13+
UserPreferenceResourceActions,
14+
UpdatedUserPreferencesType,
15+
UserPreferencesPayloadValueType,
16+
UpdateUserPreferencesPayloadType,
17+
} from './types'
18+
19+
/**
20+
* @returns UserPreferencesType
21+
* @description This function fetches the user preferences from the server. It uses the `get` method to make a request to the server and retrieves the user preferences based on the `USER_PREFERENCES_ATTRIBUTE_KEY`. The result is parsed and returned as a `UserPreferencesType` object.
22+
* @throws Will throw an error if the request fails or if the result is not in the expected format.
23+
*/
24+
export const getUserPreferences = async (): Promise<UserPreferencesType> => {
25+
const queryParamsPayload: Pick<GetUserPreferencesQueryParamsType, 'key'> = {
26+
key: USER_PREFERENCES_ATTRIBUTE_KEY,
27+
}
28+
29+
const { result } = await get<{ value: string }>(
30+
getUrlWithSearchParams(`${ROUTES.ATTRIBUTES_USER}/${ROUTES.GET}`, queryParamsPayload),
31+
)
32+
33+
if (!result?.value) {
34+
return null
35+
}
36+
37+
const parsedResult: GetUserPreferencesParsedDTO = JSON.parse(result.value)
38+
39+
const pipelineRBACViewSelectedTab = parsedResult.viewPermittedEnvOnly
40+
? ViewIsPipelineRBACConfiguredRadioTabs.ACCESS_ONLY
41+
: ViewIsPipelineRBACConfiguredRadioTabs.ALL_ENVIRONMENTS
42+
43+
return {
44+
pipelineRBACViewSelectedTab,
45+
themePreference:
46+
parsedResult.computedAppTheme === 'system-dark' || parsedResult.computedAppTheme === 'system-light'
47+
? THEME_PREFERENCE_MAP.auto
48+
: parsedResult.computedAppTheme,
49+
resources: parsedResult.resources,
50+
}
51+
}
52+
53+
export const resourceTypes: ResourceKindType[] = [ResourceKindType.devtronApplication]
54+
55+
const resourcesObj = (recentlyVisited: BaseAppMetaData[]): UserPreferenceResourceType =>
56+
resourceTypes.reduce((acc, resource) => {
57+
acc[resource] = {
58+
[UserPreferenceResourceActions.RECENTLY_VISITED]: recentlyVisited,
59+
}
60+
return acc
61+
}, {} as UserPreferenceResourceType)
62+
63+
/**
64+
* @description This function updates the user preferences in the server. It constructs a payload with the updated user preferences and sends a PATCH request to the server. If the request is successful, it returns true. If an error occurs, it shows an error message and returns false.
65+
* @param updatedUserPreferences - The updated user preferences to be sent to the server.
66+
* @param recentlyVisitedDevtronApps - The recently visited Devtron apps to be sent to the server.
67+
* @param shouldThrowError - A boolean indicating whether to throw an error if the request fails. Default is false.
68+
* @returns A promise that resolves to true if the request is successful, or false if an error occurs.
69+
* @throws Will throw an error if `shouldThrowError` is true and the request fails.
70+
*/
71+
72+
export const updateUserPreferences = async (
73+
updatedUserPreferences?: UpdatedUserPreferencesType,
74+
recentlyVisitedDevtronApps?: BaseAppMetaData[],
75+
shouldThrowError: boolean = false,
76+
): Promise<boolean> => {
77+
try {
78+
let value: UserPreferencesPayloadValueType = null
79+
if (updatedUserPreferences) {
80+
const { themePreference, appTheme, pipelineRBACViewSelectedTab } = updatedUserPreferences
81+
82+
value = {
83+
viewPermittedEnvOnly: pipelineRBACViewSelectedTab === ViewIsPipelineRBACConfiguredRadioTabs.ACCESS_ONLY,
84+
computedAppTheme: themePreference === THEME_PREFERENCE_MAP.auto ? `system-${appTheme}` : appTheme,
85+
}
86+
}
87+
88+
if (recentlyVisitedDevtronApps?.length) {
89+
value = {
90+
resources: resourcesObj(recentlyVisitedDevtronApps),
91+
}
92+
}
93+
94+
const payload: UpdateUserPreferencesPayloadType = {
95+
key: USER_PREFERENCES_ATTRIBUTE_KEY,
96+
value: JSON.stringify(value),
97+
}
98+
99+
await patch(`${ROUTES.ATTRIBUTES_USER}/${ROUTES.PATCH}`, payload)
100+
return true
101+
} catch (error) {
102+
if (shouldThrowError) {
103+
throw error
104+
}
105+
106+
showError(error)
107+
return false
108+
}
109+
}
110+
111+
export type UserPreferencesUpdateType =
112+
| { type: 'updateTheme'; value: ThemePreferenceType | null; appTheme: ThemeConfigType['appTheme'] }
113+
| { type: 'updatePipelineRBACView'; value: ViewIsPipelineRBACConfiguredRadioTabs }
114+
| { type: 'updateRecentlyVisitedApps'; value: BaseAppMetaData[] }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { AppThemeType, BaseAppMetaData, ResourceKindType } from '@Shared/index'
2+
import { ThemeConfigType, ThemePreferenceType } from '@Shared/Providers/ThemeProvider/types'
3+
import { USER_PREFERENCES_ATTRIBUTE_KEY } from '@Shared/Hooks/useUserPreferences/constants'
4+
5+
export interface GetUserPreferencesQueryParamsType {
6+
key: typeof USER_PREFERENCES_ATTRIBUTE_KEY
7+
}
8+
9+
export enum ViewIsPipelineRBACConfiguredRadioTabs {
10+
ALL_ENVIRONMENTS = 'All environments',
11+
ACCESS_ONLY = 'Access only',
12+
}
13+
14+
export enum UserPreferenceResourceActions {
15+
RECENTLY_VISITED = 'recently-visited',
16+
}
17+
export interface UserResourceKindActionType {
18+
[UserPreferenceResourceActions.RECENTLY_VISITED]: BaseAppMetaData[]
19+
}
20+
export interface UserPreferenceResourceType {
21+
[ResourceKindType.devtronApplication]: UserResourceKindActionType
22+
}
23+
export interface GetUserPreferencesParsedDTO {
24+
viewPermittedEnvOnly?: boolean
25+
/**
26+
* Computed app theme for the user
27+
*
28+
* Could be 'light' | 'dark' | 'system-light' | 'system-dark'
29+
*/
30+
computedAppTheme?: AppThemeType | `system-${AppThemeType}`
31+
/**
32+
* @description resources object with key as resource kind and value as ResourceType
33+
*
34+
*/
35+
resources?: UserPreferenceResourceType
36+
}
37+
export interface UserPreferencesPayloadValueType extends GetUserPreferencesParsedDTO {}
38+
export interface UpdateUserPreferencesPayloadType extends Pick<GetUserPreferencesQueryParamsType, 'key'> {
39+
value: string
40+
}
41+
export interface UserPreferencesType {
42+
/**
43+
* Preferred theme for the user
44+
* If null, would forcibly show user theme switcher dialog for user to select
45+
*/
46+
themePreference?: ThemePreferenceType | null
47+
/**
48+
* @type {ViewIsPipelineRBACConfiguredRadioTabs}
49+
* @description pipelineRBACViewSelectedTab is used to store the selected tab in the pipeline RBAC view
50+
* @default ViewIsPipelineRBACConfiguredRadioTabs.VIEW_PERMITTED_ENV
51+
*/
52+
pipelineRBACViewSelectedTab?: ViewIsPipelineRBACConfiguredRadioTabs
53+
/**
54+
* @description resources object
55+
*/
56+
resources?: GetUserPreferencesParsedDTO['resources']
57+
}
58+
59+
export interface UpdatedUserPreferencesType extends UserPreferencesType, Pick<ThemeConfigType, 'appTheme'> {}
60+
61+
export interface UseUserPreferencesProps {
62+
migrateUserPreferences?: (userPreferencesResponse: UserPreferencesType) => Promise<UserPreferencesType>
63+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ResourceKindType, useTheme } from '@Shared/index'
2+
import { useState } from 'react'
3+
import { ServerErrors } from '@Common/ServerError'
4+
import {
5+
UserPreferenceResourceActions,
6+
UserPreferencesType,
7+
UseUserPreferencesProps,
8+
ViewIsPipelineRBACConfiguredRadioTabs,
9+
} from './types'
10+
import { getUserPreferences } from './service'
11+
12+
export const useUserPreferences = ({ migrateUserPreferences }: UseUserPreferencesProps) => {
13+
const [userPreferences, setUserPreferences] = useState<UserPreferencesType>(null)
14+
const [userPreferencesError, setUserPreferencesError] = useState<ServerErrors>(null)
15+
16+
const { handleThemeSwitcherDialogVisibilityChange, handleThemePreferenceChange } = useTheme()
17+
18+
const fetchRecentlyVisitedParsedApps = async (appId: number, appName: string, isInvalidAppId: boolean = false) => {
19+
const userPreferencesResponse = await getUserPreferences()
20+
const _recentApps =
21+
userPreferencesResponse?.resources?.[ResourceKindType.devtronApplication]?.[
22+
UserPreferenceResourceActions.RECENTLY_VISITED
23+
] || []
24+
25+
// Ensure all items have valid `appId` and `appName`
26+
const validApps = _recentApps.filter((app) => app?.appId && app?.appName)
27+
28+
// Combine current app with previous list
29+
const combinedList = [{ appId, appName }, ...validApps]
30+
31+
// Filter out invalid app and limit to 6 && Ensure unique entries using a Set
32+
const uniqueApps = Array.from(new Map(combinedList.map((app) => [app.appId, app])).values()).slice(0, 6)
33+
const uniqueFilteredApps = isInvalidAppId ? uniqueApps.filter((app) => app.appId !== Number(appId)) : uniqueApps
34+
setUserPreferences((prev) => ({
35+
...prev,
36+
resources: {
37+
...prev?.resources,
38+
[ResourceKindType.devtronApplication]: {
39+
...prev?.resources?.[ResourceKindType.devtronApplication],
40+
[UserPreferenceResourceActions.RECENTLY_VISITED]: uniqueFilteredApps,
41+
},
42+
},
43+
}))
44+
}
45+
46+
const handleInitializeUserPreferencesFromResponse = (userPreferencesResponse: UserPreferencesType) => {
47+
if (!userPreferencesResponse?.themePreference) {
48+
handleThemeSwitcherDialogVisibilityChange(true)
49+
} else if (userPreferencesResponse?.themePreference) {
50+
handleThemePreferenceChange(userPreferencesResponse?.themePreference)
51+
}
52+
setUserPreferences(userPreferencesResponse)
53+
}
54+
55+
const handleFetchUserPreferences = async () => {
56+
try {
57+
setUserPreferencesError(null)
58+
const userPreferencesResponse = await getUserPreferences()
59+
if (migrateUserPreferences) {
60+
const migratedUserPreferences = await migrateUserPreferences(userPreferencesResponse)
61+
handleInitializeUserPreferencesFromResponse(migratedUserPreferences)
62+
} else {
63+
handleInitializeUserPreferencesFromResponse(userPreferencesResponse)
64+
}
65+
} catch (error) {
66+
setUserPreferencesError(error)
67+
}
68+
}
69+
70+
// To handle in case through browser prompt user cancelled the refresh
71+
const handleUpdatePipelineRBACViewSelectedTab = (selectedTab: ViewIsPipelineRBACConfiguredRadioTabs) => {
72+
setUserPreferences((prev) => ({
73+
...prev,
74+
pipelineRBACViewSelectedTab: selectedTab,
75+
}))
76+
}
77+
78+
const handleUpdateUserThemePreference = (themePreference: UserPreferencesType['themePreference']) => {
79+
setUserPreferences((prev) => ({
80+
...prev,
81+
themePreference,
82+
}))
83+
}
84+
85+
return {
86+
userPreferences,
87+
userPreferencesError,
88+
handleFetchUserPreferences,
89+
handleUpdatePipelineRBACViewSelectedTab,
90+
handleUpdateUserThemePreference,
91+
fetchRecentlyVisitedParsedApps,
92+
}
93+
}

0 commit comments

Comments
 (0)