Skip to content

Commit 5d02ca2

Browse files
authored
Merge pull request #2606 from devtron-labs/feat/recently-visited-app-selector
feat: Support for Recently visited Devtron App in async selector
2 parents 2ff6b0c + 0832446 commit 5d02ca2

File tree

26 files changed

+319
-307
lines changed

26 files changed

+319
-307
lines changed

.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ src/components/app/create/CreateApp.tsx
8686
src/components/app/create/validationRules.ts
8787
src/components/app/details/AboutAppInfoModal.tsx
8888
src/components/app/details/AboutTagEditModal.tsx
89-
src/components/app/details/AppHeader.tsx
9089
src/components/app/details/appDetails/AppMetrics.tsx
9190
src/components/app/details/appDetails/AppStatusCard.tsx
9291
src/components/app/details/appDetails/DeploymentStatusDetailModal.tsx

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"homepage": "/dashboard",
66
"dependencies": {
7-
"@devtron-labs/devtron-fe-common-lib": "1.10.16",
7+
"@devtron-labs/devtron-fe-common-lib": "1.10.17",
88
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
99
"@rjsf/core": "^5.13.3",
1010
"@rjsf/utils": "^5.13.3",

src/Pages/App/CreateAppModal/AppToCloneSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const AppToCloneSelector = ({ isJobView, error, handleCloneIdChange }: AppToClon
3333
const onInputChange: SelectPickerProps['onInputChange'] = async (val) => {
3434
setInputValue(val)
3535
setAreOptionsLoading(true)
36-
const fetchedOptions = await appListOptions(val, isJobView)
36+
const fetchedOptions = await appListOptions({ inputValue: val, isJobView })
3737
setAreOptionsLoading(false)
3838
setOptions(fetchedOptions)
3939
}

src/Pages/GlobalConfigurations/Authorization/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import {
3232
UserRoleConfig,
3333
APIOptions,
3434
ActionTypes,
35+
DeleteConfirmationModalProps,
3536
} from '@devtron-labs/devtron-fe-common-lib'
36-
import { DeleteConfirmationModalProps } from '@devtron-labs/devtron-fe-common-lib/dist/Shared/Components/ConfirmationModal/types'
3737
import { SERVER_MODE } from '../../../config'
3838
import { PermissionType, UserRoleType } from './constants'
3939

src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { TagType } from '@devtron-labs/devtron-fe-common-lib'
18-
import { DeleteConfirmationModalProps } from '@devtron-labs/devtron-fe-common-lib/dist/Shared/Components/ConfirmationModal/types'
17+
import { DeleteConfirmationModalProps, TagType } from '@devtron-labs/devtron-fe-common-lib'
1918

2019
export interface ClusterEnvironmentDrawerFormProps {
2120
environmentName: string

src/Pages/Shared/SwitchThemeDialog/SwitchThemeDialog.component.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ const ThemePreferenceOption = ({
124124
const SwitchThemeDialog = ({
125125
initialThemePreference,
126126
handleClose,
127-
currentUserPreferences,
128127
handleUpdateUserThemePreference,
129128
disableAPICalls = false,
130129
}: SwitchThemeDialogProps) => {
@@ -166,7 +165,11 @@ const SwitchThemeDialog = ({
166165
setIsSaving(true)
167166

168167
if (!disableAPICalls) {
169-
const isSuccessful = await updateUserPreferences({ ...currentUserPreferences, themePreference, appTheme })
168+
const isSuccessful = await updateUserPreferences({
169+
path: 'themePreference',
170+
value: { themePreference, appTheme },
171+
})
172+
170173
if (isSuccessful) {
171174
handleSuccess()
172175
}

src/Pages/Shared/SwitchThemeDialog/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { AppThemeType, UserPreferencesType, useTheme } from '@devtron-labs/devtron-fe-common-lib'
17+
import { AppThemeType, useTheme } from '@devtron-labs/devtron-fe-common-lib'
1818

1919
type ThemePreferenceType = ReturnType<typeof useTheme>['themePreference']
2020

@@ -27,7 +27,6 @@ export type SwitchThemeDialogProps = {
2727
handleClose: () => void
2828
} & (
2929
| {
30-
currentUserPreferences: UserPreferencesType
3130
/**
3231
* @default false
3332
* @description Required for storybook

src/components/AppSelector/AppSelector.tsx

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,58 +14,115 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useRef } from 'react'
17+
import { useRef, useState } from 'react'
18+
import ReactGA from 'react-ga4'
1819
import {
19-
abortPreviousRequests,
20-
APP_SELECTOR_STYLES,
21-
AppSelectorDropdownIndicator,
22-
AppSelectorNoOptionsMessage,
20+
ComponentSizeType,
21+
SelectPicker,
22+
SelectPickerProps,
23+
SelectPickerVariantType,
24+
useAsync,
25+
ResourceKindType,
26+
UserPreferenceResourceActions,
27+
BaseAppMetaData,
28+
getNoMatchingResultText,
29+
useUserPreferences,
30+
AppSelectorNoOptionsMessage as appSelectorNoOptionsMessage,
31+
SelectPickerOptionType,
2332
} from '@devtron-labs/devtron-fe-common-lib'
24-
import { Props as SelectProps, SelectInstance } from 'react-select'
25-
import AsyncSelect from 'react-select/async'
33+
import { ActionMeta } from 'react-select'
2634
import { appListOptions } from './AppSelectorUtil'
27-
28-
interface AppSelectorType {
29-
onChange: ({ label, value }) => void
30-
appId: number
31-
appName: string
32-
isJobView?: boolean
33-
}
35+
import { AppSelectorType, RecentlyVisitedOptions } from './AppSelector.types'
36+
import { APP_DETAILS_GA_EVENTS } from './constants'
3437

3538
const AppSelector = ({ onChange, appId, appName, isJobView }: AppSelectorType) => {
36-
const selectRef = useRef<SelectInstance>(null)
37-
3839
const abortControllerRef = useRef<AbortController>(new AbortController())
3940

40-
const defaultOptions = [{ value: appId, label: appName }]
41-
const loadAppListOptions = (inputValue: string) =>
42-
abortPreviousRequests(
43-
() => appListOptions(inputValue, isJobView, abortControllerRef.current.signal),
44-
abortControllerRef,
45-
)
41+
const { userPreferences, fetchRecentlyVisitedParsedApps } = useUserPreferences({})
42+
const [inputValue, setInputValue] = useState('')
43+
44+
const recentlyVisitedDevtronApps =
45+
userPreferences?.resources?.[ResourceKindType.devtronApplication]?.[
46+
UserPreferenceResourceActions.RECENTLY_VISITED
47+
] || ([] as BaseAppMetaData[])
48+
49+
const isAppDataAvailable = !!appId && !!appName
50+
const shouldFetchAppOptions = isJobView ? true : !!recentlyVisitedDevtronApps.length
51+
52+
const [loading, selectOptions] = useAsync(
53+
() =>
54+
appListOptions({
55+
inputValue,
56+
isJobView,
57+
signal: abortControllerRef.current.signal,
58+
recentlyVisitedDevtronApps,
59+
}),
60+
[inputValue, isJobView],
61+
isAppDataAvailable && shouldFetchAppOptions,
62+
)
63+
64+
// fetching recently visited apps only in case of devtron apps
65+
useAsync(
66+
() => fetchRecentlyVisitedParsedApps({ appId, appName }),
67+
[appId, appName],
68+
isAppDataAvailable && !isJobView,
69+
)
70+
71+
const onInputChange: SelectPickerProps['onInputChange'] = async (val) => {
72+
setInputValue(val)
73+
}
74+
75+
const customSelect: SelectPickerProps['filterOption'] = (option, searchText: string) => {
76+
const label = option.data.label as string
77+
return option.data.value === 0 || label.toLowerCase().includes(searchText.toLowerCase())
78+
}
79+
80+
const getDisabledOptions = (option: RecentlyVisitedOptions): SelectPickerProps['isDisabled'] => option.isDisabled
81+
82+
const noOptionsMessage = () =>
83+
isJobView
84+
? appSelectorNoOptionsMessage({
85+
inputValue,
86+
})
87+
: getNoMatchingResultText()
88+
89+
const _selectOption = selectOptions?.map((section) => ({
90+
...section,
91+
options: section.label === 'Recently Visited' ? section.options.slice(1) : section.options,
92+
}))
93+
94+
const handleChange = (
95+
selectedOption: RecentlyVisitedOptions,
96+
actionMeta: ActionMeta<SelectPickerOptionType<string | number>>,
97+
) => {
98+
if (selectedOption.label === appName) return
99+
100+
onChange(selectedOption, actionMeta)
46101

47-
const handleOnKeyDown: SelectProps['onKeyDown'] = (event) => {
48-
if (event.key === 'Escape') {
49-
selectRef.current?.inputRef.blur()
102+
if (!isJobView) {
103+
ReactGA.event(
104+
selectedOption.isRecentlyVisited
105+
? APP_DETAILS_GA_EVENTS.RecentlyVisitedApps
106+
: APP_DETAILS_GA_EVENTS.SearchesAppClicked,
107+
)
50108
}
51109
}
52110

53111
return (
54-
<AsyncSelect
55-
ref={selectRef}
56-
blurInputOnSelect
57-
onKeyDown={handleOnKeyDown}
58-
defaultOptions
59-
loadOptions={loadAppListOptions}
60-
noOptionsMessage={AppSelectorNoOptionsMessage}
61-
onChange={onChange}
62-
components={{
63-
IndicatorSeparator: null,
64-
DropdownIndicator: AppSelectorDropdownIndicator,
65-
LoadingIndicator: null,
66-
}}
67-
value={defaultOptions[0]}
68-
styles={APP_SELECTOR_STYLES}
112+
<SelectPicker
113+
inputId={`${isJobView ? 'job' : 'app'}-name`}
114+
options={_selectOption || []}
115+
inputValue={inputValue}
116+
onInputChange={onInputChange}
117+
isLoading={loading}
118+
noOptionsMessage={noOptionsMessage}
119+
onChange={handleChange}
120+
value={{ value: appId, label: appName }}
121+
variant={SelectPickerVariantType.BORDER_LESS}
122+
placeholder={appName}
123+
isOptionDisabled={getDisabledOptions}
124+
size={ComponentSizeType.xl}
125+
filterOption={customSelect}
69126
/>
70127
)
71128
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { AppHeaderType } from '@Components/app/types'
2+
import { BaseAppMetaData, SelectPickerOptionType, SelectPickerProps } from '@devtron-labs/devtron-fe-common-lib'
3+
import { GroupBase } from 'react-select'
4+
5+
export interface AppSelectorType extends Pick<SelectPickerProps, 'onChange'>, Pick<AppHeaderType, 'appName'> {
6+
appId: number
7+
isJobView?: boolean
8+
}
9+
10+
export interface RecentlyVisitedOptions extends SelectPickerOptionType<number> {
11+
isDisabled?: boolean
12+
isRecentlyVisited?: boolean
13+
}
14+
15+
export interface RecentlyVisitedGroupedOptionsType extends GroupBase<SelectPickerOptionType<number>> {
16+
label: string
17+
options: RecentlyVisitedOptions[]
18+
}
19+
20+
export interface AppListOptionsTypes {
21+
inputValue: string
22+
isJobView?: boolean
23+
signal?: AbortSignal
24+
recentlyVisitedDevtronApps?: BaseAppMetaData[] | []
25+
}

src/components/AppSelector/AppSelectorUtil.tsx

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { getIsRequestAborted, ServerErrors, showError } from '@devtron-labs/devtron-fe-common-lib'
17+
import { BaseAppMetaData, getIsRequestAborted, ServerErrors, showError } from '@devtron-labs/devtron-fe-common-lib'
1818
import { getAppListMin } from '../../services/service'
19+
import { AppListOptionsTypes, RecentlyVisitedGroupedOptionsType, RecentlyVisitedOptions } from './AppSelector.types'
20+
import { AllApplicationsMetaData } from './constants'
1921

2022
let timeoutId
2123

22-
export const appListOptions = (inputValue: string, isJobView?: boolean, signal?: AbortSignal): Promise<[]> => {
24+
export const appListOptions = ({
25+
inputValue,
26+
isJobView = false,
27+
signal,
28+
recentlyVisitedDevtronApps,
29+
}: AppListOptionsTypes): Promise<RecentlyVisitedGroupedOptionsType[]> => {
2330
const options = signal ? { signal } : null
2431

2532
return new Promise((resolve) => {
@@ -28,29 +35,47 @@ export const appListOptions = (inputValue: string, isJobView?: boolean, signal?:
2835
}
2936
timeoutId = setTimeout(() => {
3037
if (inputValue.length < 3) {
31-
resolve([])
32-
return
33-
}
34-
getAppListMin(null, options, inputValue, isJobView ?? false)
35-
.then((response) => {
36-
let appList = []
37-
if (response.result) {
38-
appList = response.result.map((res) => ({
39-
value: res.id,
40-
label: res.name,
41-
...res,
42-
}))
43-
}
44-
resolve(appList as [])
45-
})
46-
.catch((errors: ServerErrors) => {
47-
if (!getIsRequestAborted(errors)) {
48-
resolve([])
49-
if (errors.code) {
50-
showError(errors)
38+
resolve(
39+
recentlyVisitedDevtronApps?.length && !isJobView
40+
? [
41+
{
42+
label: 'Recently Visited',
43+
options: recentlyVisitedDevtronApps.map((app: BaseAppMetaData) => ({
44+
label: app.appName,
45+
value: app.appId,
46+
isRecentlyVisited: true,
47+
})) as RecentlyVisitedOptions[],
48+
},
49+
AllApplicationsMetaData,
50+
]
51+
: [],
52+
)
53+
} else {
54+
getAppListMin(null, options, inputValue, isJobView ?? false)
55+
.then((response) => {
56+
const appList = response.result
57+
? ([
58+
{
59+
label: `All ${isJobView ? 'Jobs' : 'Applications'}`,
60+
options: response.result.map((res) => ({
61+
value: res.id,
62+
label: res.name,
63+
})) as RecentlyVisitedOptions[],
64+
},
65+
] as RecentlyVisitedGroupedOptionsType[])
66+
: []
67+
68+
resolve(appList)
69+
})
70+
.catch((errors: ServerErrors) => {
71+
if (!getIsRequestAborted(errors)) {
72+
resolve([])
73+
if (errors.code) {
74+
showError(errors)
75+
}
5176
}
52-
}
53-
})
77+
})
78+
}
5479
}, 300)
5580
})
5681
}

0 commit comments

Comments
 (0)