Skip to content

Commit d6f3b6d

Browse files
committed
feat: OU-1195 [DashboardListPage] List OCP namespaces in Create Dashboard ProjectSelector
1 parent ee45e94 commit d6f3b6d

9 files changed

Lines changed: 407 additions & 275 deletions

File tree

web/locales/en/plugin__monitoring-plugin.json

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,21 +166,66 @@
166166
"Time range": "Time range",
167167
"Refresh interval": "Refresh interval",
168168
"Could not parse JSON data for dashboard \"{{dashboard}}\"": "Could not parse JSON data for dashboard \"{{dashboard}}\"",
169-
"Dashboard Variables": "Dashboard Variables",
169+
"Rename Dashboard": "Rename Dashboard",
170+
"Dashboard name": "Dashboard name",
171+
"Renaming...": "Renaming...",
172+
"Rename": "Rename",
173+
"Loading...": "Loading...",
174+
"Failed to load project permissions. Please refresh the page and try again.": "Failed to load project permissions. Please refresh the page and try again.",
175+
"Select namespace": "Select namespace",
176+
"Duplicate": "Duplicate",
177+
"this dashboard": "this dashboard",
178+
"Permanently delete dashboard?": "Permanently delete dashboard?",
179+
"Are you sure you want to delete ": "Are you sure you want to delete ",
180+
"? This action can not be undone.": "? This action can not be undone.",
181+
"Deleting...": "Deleting...",
182+
"Delete": "Delete",
183+
"Must be 75 or fewer characters long": "Must be 75 or fewer characters long",
184+
"Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!": "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!",
185+
"Project is required": "Project is required",
186+
"Dashboard name is required": "Dashboard name is required",
187+
"Failed to create dashboard. Please try again.": "Failed to create dashboard. Please try again.",
188+
"Create": "Create",
189+
"Create button is disabled because you do not have permission": "Create button is disabled because you do not have permission",
190+
"Create Dashboard": "Create Dashboard",
191+
"Select project": "Select project",
192+
"Select a project": "Select a project",
193+
"my-new-dashboard": "my-new-dashboard",
194+
"Creating...": "Creating...",
195+
"View and manage dashboards.": "View and manage dashboards.",
196+
"Rename dashboard": "Rename dashboard",
197+
"Duplicate dashboard": "Duplicate dashboard",
198+
"Delete dashboard": "Delete dashboard",
199+
"You don't have permissions to dashboard actions": "You don't have permissions to dashboard actions",
200+
"Dashboard": "Dashboard",
201+
"Project": "Project",
202+
"Created on": "Created on",
203+
"Last Modified": "Last Modified",
204+
"Filter by name": "Filter by name",
205+
"Filter by project": "Filter by project",
206+
"No dashboards found": "No dashboards found",
207+
"No results match the filter criteria. Clear filters to show results.": "No results match the filter criteria. Clear filters to show results.",
208+
"No Perses dashboards are currently available in this project.": "No Perses dashboards are currently available in this project.",
209+
"Clear all filters": "Clear all filters",
210+
"Dashboard not found": "Dashboard not found",
211+
"The dashboard \"{{name}}\" was not found in project \"{{project}}\".": "The dashboard \"{{name}}\" was not found in project \"{{project}}\".",
212+
"Empty Dashboard": "Empty Dashboard",
213+
"To get started add something to your dashboard": "To get started add something to your dashboard",
214+
"Edit": "Edit",
215+
"You don't have permission to edit this dashboard": "You don't have permission to edit this dashboard",
170216
"No matching datasource found": "No matching datasource found",
171217
"No Dashboard Available in Selected Project": "No Dashboard Available in Selected Project",
172218
"To explore data, create a dashboard for this project": "To explore data, create a dashboard for this project",
173219
"No Perses Project Available": "No Perses Project Available",
174220
"To explore data, create a Perses Project": "To explore data, create a Perses Project",
175-
"Empty Dashboard": "Empty Dashboard",
176-
"To get started add something to your dashboard": "To get started add something to your dashboard",
221+
"Project is required for fetching project dashboards": "Project is required for fetching project dashboards",
177222
"No projects found": "No projects found",
178223
"No results match the filter criteria.": "No results match the filter criteria.",
179224
"Clear filters": "Clear filters",
180225
"Select project...": "Select project...",
181226
"Projects": "Projects",
182-
"Project": "Project",
183-
"Dashboard": "Dashboard",
227+
"All Projects": "All Projects",
228+
"useToast must be used within ToastProvider": "useToast must be used within ToastProvider",
184229
"Refresh off": "Refresh off",
185230
"{{count}} second_one": "{{count}} second",
186231
"{{count}} second_other": "{{count}} seconds",
@@ -203,7 +248,7 @@
203248
"Component(s)": "Component(s)",
204249
"Alert": "Alert",
205250
"Incidents": "Incidents",
206-
"Clear all filters": "Clear all filters",
251+
"Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.": "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.",
207252
"Filter type selection": "Filter type selection",
208253
"Incident ID": "Incident ID",
209254
"Severity filters": "Severity filters",
@@ -303,6 +348,5 @@
303348
"No metrics targets found": "No metrics targets found",
304349
"Error loading latest targets data": "Error loading latest targets data",
305350
"Search by endpoint or namespace...": "Search by endpoint or namespace...",
306-
"Text": "Text",
307-
"Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.":"Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information."
351+
"Text": "Text"
308352
}

web/src/components/dashboards/perses/dashboard-action-modals.tsx

Lines changed: 51 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,11 @@ import {
1313
HelperTextItemVariant,
1414
ModalVariant,
1515
AlertVariant,
16-
Select,
17-
SelectOption,
18-
SelectList,
19-
MenuToggle,
20-
MenuToggleElement,
2116
Stack,
2217
StackItem,
2318
Spinner,
2419
} from '@patternfly/react-core';
20+
import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates';
2521
import { ExclamationCircleIcon } from '@patternfly/react-icons';
2622
import React, { useMemo, useState } from 'react';
2723
import { useTranslation } from 'react-i18next';
@@ -46,9 +42,8 @@ import {
4642
getResourceExtendedDisplayName,
4743
} from '@perses-dev/core';
4844
import { useToast } from './ToastProvider';
49-
import { usePerses } from './hooks/usePerses';
5045
import { generateMetadataName } from './dashboard-utils';
51-
import { useProjectPermissions } from './dashboard-permissions';
46+
import { useEditableProjects } from './hooks/useEditableProjects';
5247
import { t_global_spacer_200, t_global_font_weight_200 } from '@patternfly/react-tokens';
5348
import { useNavigate } from 'react-router-dom-v5-compat';
5449
import { usePerspective, getDashboardUrl } from '../../hooks/usePerspective';
@@ -189,19 +184,15 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
189184

190185
const navigate = useNavigate();
191186
const { perspective } = usePerspective();
192-
const [isProjectSelectOpen, setIsProjectSelectOpen] = useState(false);
187+
const [selectedProject, setSelectedProject] = useState<string | null>(null);
193188

194-
const { persesProjects, persesProjectsLoading } = usePerses();
195-
196-
const hookInput = useMemo(() => {
197-
return persesProjects || [];
198-
}, [persesProjects]);
199-
200-
const { editableProjects } = useProjectPermissions(hookInput);
201-
202-
const filteredProjects = useMemo(() => {
203-
return persesProjects.filter((project) => editableProjects.includes(project.metadata.name));
204-
}, [persesProjects, editableProjects]);
189+
const {
190+
editableProjects,
191+
allProjects,
192+
hasEditableProject,
193+
permissionsLoading,
194+
permissionsError,
195+
} = useEditableProjects();
205196

206197
const defaultProject = useMemo(() => {
207198
if (!dashboard) return '';
@@ -210,8 +201,8 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
210201
return dashboard.metadata.project;
211202
}
212203

213-
return filteredProjects[0]?.metadata.name || '';
214-
}, [dashboard, editableProjects, filteredProjects]);
204+
return allProjects[0] || '';
205+
}, [dashboard, editableProjects, allProjects]);
215206

216207
const { schema: validationSchema } = useDashboardValidationSchema(defaultProject, t);
217208

@@ -226,24 +217,29 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
226217
},
227218
});
228219

220+
const selectedProjectName = form.watch('projectName');
221+
222+
const projectOptions = useMemo<TypeaheadSelectOption[]>(() => {
223+
if (!editableProjects) {
224+
return [];
225+
}
226+
return editableProjects.map((project) => ({
227+
content: project,
228+
value: project,
229+
selected: project === selectedProjectName,
230+
}));
231+
}, [editableProjects, selectedProjectName]);
232+
229233
const createDashboardMutation = useCreateDashboardMutation();
230234

231235
React.useEffect(() => {
232-
if (isOpen && dashboard && filteredProjects.length > 0 && defaultProject) {
236+
if (isOpen && dashboard && editableProjects?.length > 0 && defaultProject) {
233237
form.reset({
234238
projectName: defaultProject,
235239
dashboardName: '',
236240
});
237241
}
238-
}, [isOpen, dashboard, defaultProject, filteredProjects.length, form]);
239-
240-
const selectedProjectName = form.watch('projectName');
241-
const selectedProjectDisplay = useMemo(() => {
242-
const selectedProject = filteredProjects.find((p) => p.metadata.name === selectedProjectName);
243-
return selectedProject
244-
? getResourceDisplayName(selectedProject)
245-
: selectedProjectName || t('Select project');
246-
}, [filteredProjects, selectedProjectName, t]);
242+
}, [isOpen, dashboard, defaultProject, editableProjects?.length, form]);
247243

248244
if (!dashboard) {
249245
return null;
@@ -295,18 +291,9 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
295291
form.reset();
296292
};
297293

298-
const onProjectToggle = () => {
299-
setIsProjectSelectOpen(!isProjectSelectOpen);
300-
};
301-
302-
const onProjectSelect = (
303-
_event: React.MouseEvent<Element, MouseEvent> | undefined,
304-
value: string | number | undefined,
305-
) => {
306-
if (typeof value === 'string') {
307-
form.setValue('projectName', value);
308-
setIsProjectSelectOpen(false);
309-
}
294+
const onProjectSelect = (_event: any, selection: string) => {
295+
form.setValue('projectName', selection);
296+
setSelectedProject(selection);
310297
};
311298

312299
return (
@@ -318,10 +305,15 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
318305
aria-labelledby="duplicate-modal"
319306
>
320307
<ModalHeader title="Duplicate Dashboard" labelId="duplicate-modal-title" />
321-
{persesProjectsLoading ? (
308+
{permissionsLoading ? (
322309
<ModalBody style={{ textAlign: 'center', padding: '2rem' }}>
323310
{t('Loading...')} <Spinner aria-label="Duplicate Dashboard Modal Loading" />
324311
</ModalBody>
312+
) : permissionsError ? (
313+
<ModalBody style={{ textAlign: 'center', padding: '2rem' }}>
314+
<ExclamationCircleIcon />
315+
{t('Failed to load project permissions. Please refresh the page and try again.')}
316+
</ModalBody>
325317
) : (
326318
<FormProvider {...form}>
327319
<form onSubmit={form.handleSubmit(processForm)}>
@@ -368,42 +360,28 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
368360
<Controller
369361
control={form.control}
370362
name="projectName"
371-
render={({ field, fieldState }) => (
363+
render={({ fieldState }) => (
372364
<FormGroup
373365
label={t('Select namespace')}
374366
isRequired
375367
fieldId="duplicate-modal-select-namespace-form-group"
376368
style={formGroupStyle}
377369
>
378370
<LabelSpacer />
379-
<Select
380-
id="duplicate-modal-select-namespace-form-group-select"
381-
isOpen={isProjectSelectOpen}
382-
selected={field.value}
371+
<TypeaheadSelect
372+
key={selectedProject || 'no-selection'}
373+
initialOptions={projectOptions}
374+
placeholder={t('Select namespace')}
375+
noOptionsFoundMessage={(filter) =>
376+
t(`No namespace found for "${filter}"`)
377+
}
378+
onClearSelection={() => {
379+
setSelectedProject(null);
380+
}}
383381
onSelect={onProjectSelect}
384-
onOpenChange={setIsProjectSelectOpen}
385-
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
386-
<MenuToggle
387-
ref={toggleRef}
388-
onClick={onProjectToggle}
389-
isExpanded={isProjectSelectOpen}
390-
isFullWidth
391-
>
392-
{selectedProjectDisplay}
393-
</MenuToggle>
394-
)}
395-
>
396-
<SelectList>
397-
{filteredProjects.map((project) => (
398-
<SelectOption
399-
key={project.metadata.name}
400-
value={project.metadata.name}
401-
>
402-
{getResourceDisplayName(project)}
403-
</SelectOption>
404-
))}
405-
</SelectList>
406-
</Select>
382+
isCreatable={false}
383+
maxMenuHeight="200px"
384+
/>
407385
{fieldState.error && (
408386
<FormHelperText>
409387
<HelperText>
@@ -430,6 +408,7 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
430408
isDisabled={
431409
!(form.watch('dashboardName') || '')?.trim() ||
432410
!(form.watch('projectName') || '')?.trim() ||
411+
!hasEditableProject ||
433412
createDashboardMutation.isPending
434413
}
435414
isLoading={createDashboardMutation.isPending}

web/src/components/dashboards/perses/dashboard-api.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { DashboardResource } from '@perses-dev/core';
1+
import { DashboardResource, ProjectResource } from '@perses-dev/core';
22
import buildURL from './perses/url-builder';
33
import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query';
44
import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk';
55
import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
66
import { StatusError } from '@perses-dev/core';
7+
import { PERSES_PROXY_BASE_PATH } from './perses-client';
78

89
const resource = 'dashboards';
910

@@ -121,3 +122,36 @@ export function useDashboardList(
121122
...options,
122123
});
123124
}
125+
126+
export const createPersesProject = async (projectName: string): Promise<ProjectResource> => {
127+
const createProjectURL = '/api/v1/projects';
128+
const persesURL = `${PERSES_PROXY_BASE_PATH}${createProjectURL}`;
129+
130+
const newProject: Partial<ProjectResource> = {
131+
kind: 'Project',
132+
metadata: {
133+
name: projectName,
134+
version: 0,
135+
},
136+
spec: {
137+
display: {
138+
name: projectName,
139+
},
140+
},
141+
};
142+
143+
return consoleFetchJSON.post(persesURL, newProject);
144+
};
145+
146+
export const useCreateProjectMutation = (): UseMutationResult<ProjectResource, Error, string> => {
147+
const queryClient = useQueryClient();
148+
149+
return useMutation<ProjectResource, Error, string>({
150+
mutationKey: ['projects'],
151+
mutationFn: createPersesProject,
152+
onSuccess: () => {
153+
queryClient.invalidateQueries({ queryKey: ['projects'] });
154+
queryClient.invalidateQueries({ queryKey: [resource] });
155+
},
156+
});
157+
};

0 commit comments

Comments
 (0)