Skip to content
Merged
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
5 changes: 3 additions & 2 deletions galasa-ui/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,14 @@
"modalHeading": "Tags im Testlauf bearbeiten",
"modalPrimaryButton": "Speichern",
"modalLabelText": "Geben Sie neue Tag-Namen zum Hinzufügen ein oder entfernen Sie vorhandene Tags aus dem Testlauf",
"modalPlaceholderText": "Geben Sie hier einen neuen Tag(s) ein und drücken Sie die [Eingabetaste]",
"modalPlaceholderText": "Geben Sie hier ein neues Tag ein",
"removeTag": "Tag entfernen",
"modalSecondaryButton": "Abbrechen",
"updateSuccess": "Tags erfolgreich aktualisiert",
"updateSuccessMessage": "Die Tags wurden für diesen Testlauf aktualisiert.",
"updateError": "Fehler beim Aktualisieren der Tags",
"updateErrorMessage": "Beim Aktualisieren der Tags ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
"updateErrorMessage": "Beim Aktualisieren der Tags ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"editTags": "Tags bearbeiten"
},
"3270Tab": {
"Terminal": "Terminal",
Expand Down
5 changes: 3 additions & 2 deletions galasa-ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,14 @@
"modalHeading": "Edit tags on test run",
"modalPrimaryButton": "Save",
"modalLabelText": "Type new tag names to add, or remove existing tags from test run",
"modalPlaceholderText": "Type new tag(s) here and hit [enter]",
"modalPlaceholderText": "Type new tag here",
"removeTag": "Remove tag",
"modalSecondaryButton": "Cancel",
"updateSuccess": "Tags updated successfully",
"updateSuccessMessage": "The tags have been updated for this test run.",
"updateError": "Failed to update tags",
"updateErrorMessage": "An error occurred while updating the tags. Please try again."
"updateErrorMessage": "An error occurred while updating the tags. Please try again.",
"editTags": "Edit tags"
},
"3270Tab": {
"Terminal": "Terminal",
Expand Down
25 changes: 24 additions & 1 deletion galasa-ui/src/actions/runsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
'use server';

import { ResultArchiveStoreAPIApi } from '@/generated/galasaapi';
import { ResultArchiveStoreAPIApi, TagsAPIApi } from '@/generated/galasaapi';
import { createAuthenticatedApiConfiguration } from '@/utils/api';
import { CLIENT_API_VERSION } from '@/utils/constants/common';

Expand Down Expand Up @@ -69,3 +69,26 @@ export const updateRunTags = async (runId: string, tags: string[]) => {
};
}
};

export const getExistingTagObjects = async () => {
try {
const apiConfig = createAuthenticatedApiConfiguration();
const tagsApiClient = new TagsAPIApi(apiConfig);

const tagsResponse = await tagsApiClient.getTags();

// Convert to plain objects and extract tag names.
const tagNames = tagsResponse
.map((tag) => tag.metadata?.name)
.filter((name): name is string => name !== undefined && name !== null);

return { success: true, tags: tagNames };
} catch (error: any) {
console.error('Error getting existing tags:', error);
return {
success: false,
error: error.message || 'Failed to get existing tags',
tags: [],
};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function TestRunsTable({
if (value.length === 0) {
return <TableCell>N/A</TableCell>;
}
const tagsArray = value.split(', ');
const tagsArray = value.split(', ').sort();
return (
<TableCell className={styles.linkCell}>
<RenderTags tags={tagsArray} isDismissible={false} size="sm" />
Expand Down
128 changes: 89 additions & 39 deletions galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@
* SPDX-License-Identifier: EPL-2.0
*/
'use client';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import styles from '@/styles/test-runs/test-run-details/OverviewTab.module.css';
import InlineText from './InlineText';
import { RunMetadata } from '@/utils/interfaces';
import { useTranslations } from 'next-intl';
import { Link, InlineNotification } from '@carbon/react';
import { Link, InlineNotification, FilterableMultiSelect, Modal } from '@carbon/react';
import { Launch, Edit } from '@carbon/icons-react';
import { getAWeekBeforeSubmittedTime } from '@/utils/timeOperations';
import useHistoryBreadCrumbs from '@/hooks/useHistoryBreadCrumbs';
import { TEST_RUNS_QUERY_PARAMS } from '@/utils/constants/common';
import { TextInput } from '@carbon/react';
import { Modal } from '@carbon/react';
import { TIME_TO_WAIT_BEFORE_CLOSING_TAG_EDIT_MODAL_MS } from '@/utils/constants/common';
import RenderTags from './RenderTags';
import { updateRunTags } from '@/actions/runsAction';
import RenderTags from '@/components/test-runs/test-run-details/RenderTags';
import { updateRunTags, getExistingTagObjects } from '@/actions/runsAction';

type DisplayedTagType = {
id: string;
label: string;
};

const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
const translations = useTranslations('OverviewTab');
Expand All @@ -27,9 +30,11 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
const [weekBefore, setWeekBefore] = useState<string | null>(null);

const [tags, setTags] = useState<string[]>(metadata?.tags || []);
const [existingTagObjectNames, setExistingTagObjectNames] = useState<string[]>([]);

const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState<boolean>(false);
const [newTagInput, setNewTagInput] = useState<string>('');
const [stagedTags, setStagedTags] = useState<Set<string>>(new Set(tags));
const [filterInput, setFilterInput] = useState<string>('');
const [stagedTags, setStagedTags] = useState<Set<string>>(new Set());
const [notification, setNotification] = useState<{
kind: 'success' | 'error';
title: string;
Expand All @@ -41,6 +46,22 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
const OTHER_RECENT_RUNS = `/test-runs?${TEST_RUNS_QUERY_PARAMS.TEST_NAME}=${fullTestName}&${TEST_RUNS_QUERY_PARAMS.BUNDLE}=${metadata?.bundle}&${TEST_RUNS_QUERY_PARAMS.PACKAGE}=${metadata?.package}&${TEST_RUNS_QUERY_PARAMS.DURATION}=60,0,0&${TEST_RUNS_QUERY_PARAMS.TAB}=results&${TEST_RUNS_QUERY_PARAMS.QUERY_NAME}=Recent runs of test ${metadata?.testName}`;
const RETRIES_FOR_THIS_TEST_RUN = `/test-runs?${TEST_RUNS_QUERY_PARAMS.SUBMISSION_ID}=${metadata?.submissionId}&${TEST_RUNS_QUERY_PARAMS.FROM}=${weekBefore}&${TEST_RUNS_QUERY_PARAMS.TAB}=results&${TEST_RUNS_QUERY_PARAMS.QUERY_NAME}=All attempts of test run ${metadata?.runName}`;

useEffect(() => {
const fetchExistingTags = async () => {
try {
const result = await getExistingTagObjects();
setExistingTagObjectNames(result.tags || []);

if (!result.success) {
console.error('Failed to fetch existing tags:', result.error);
}
} catch (error) {
console.error('Error fetching existing tags:', error);
}
};
fetchExistingTags();
}, []);

useEffect(() => {
const validateTime = () => {
const validatedTime = getAWeekBeforeSubmittedTime(metadata?.rawSubmittedAt!);
Expand Down Expand Up @@ -68,34 +89,42 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
});
};

const handleStageNewTags = () => {
// Parse new tags from input (comma or space separated).
const newTags = newTagInput
.split(/[,\s]+/)
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
const handleFilterableMultiSelectChange = (selectedItems: DisplayedTagType[]) => {
// Update staged tags based on selected items.
const newStagedTags = new Set(selectedItems.map((item) => item.label));
setStagedTags(newStagedTags);
};

// Add new tags to staged tags Set (automatically handles duplicates).
setStagedTags((prev) => {
const newSet = new Set(prev);
newTags.forEach((tag) => newSet.add(tag));
return newSet;
});
// Create items for FilterableMultiSelect.
const filterableItems = useMemo(() => {
const stagedAndExistingTags = new Set<string>();

// Clear the input after staging
setNewTagInput('');
};
// Collect all unique tag names.
stagedTags.forEach((tagName) => stagedAndExistingTags.add(tagName));
existingTagObjectNames.forEach((tagName) => stagedAndExistingTags.add(tagName));

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleStageNewTags();
// Add the current filter input if it's not empty and not already in the list.
if (filterInput.trim() && !stagedAndExistingTags.has(filterInput.trim())) {
stagedAndExistingTags.add(filterInput.trim());
}
};

const arrayOfStagedAndExistingTags = Array.from(stagedAndExistingTags);

// Create items with consistent IDs based on sorted order.
return arrayOfStagedAndExistingTags.map((tagName, index) => ({
id: `tag-${index}-${tagName}`,
label: tagName,
}));
}, [existingTagObjectNames, stagedTags, filterInput]);

// Get initially selected items based on staged tags.
const initialSelectedItems = useMemo(() => {
return filterableItems.filter((item) => stagedTags.has(item.label));
}, [filterableItems, stagedTags]);

const handleModalClose = () => {
setIsTagsEditModalOpen(false);
setNewTagInput('');
setFilterInput('');
setNotification(null);
};

Expand All @@ -117,8 +146,8 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
subtitle: translations('updateSuccessMessage'),
});

// Set tags of the component to the staged tags tags.
setTags(Array.from(stagedTags));
// Set tags of the component to the staged tags.
setTags(Array.from(stagedTags).sort());

// Close modal after a short delay to show success message.
setTimeout(() => {
Expand Down Expand Up @@ -161,13 +190,25 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
<div
className={styles.tagsEditButtonWrapper}
onClick={() => {
// Initialise staged tags from current tags when opening modal.
setStagedTags(new Set(tags));
setIsTagsEditModalOpen(true);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setStagedTags(new Set(tags));
setIsTagsEditModalOpen(true);
}
}}
tabIndex={0}
role="button"
aria-label={translations('editTags')}
>
<Edit className={styles.tagsEditButton} />
</div>
</h5>
<RenderTags tags={tags} isDismissible={false} size="md" />
<RenderTags tags={tags.sort()} isDismissible={false} size="md" />

<div className={styles.redirectLinks}>
<div className={styles.linkWrapper} onClick={handleNavigationClick}>
Expand All @@ -194,6 +235,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
secondaryButtonText={translations('modalSecondaryButton')}
onRequestSubmit={handleSaveTags}
primaryButtonDisabled={isSaving}
className={styles.tagsEditModal}
>
{notification && (
<InlineNotification
Expand All @@ -206,17 +248,25 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
onCloseButtonClick={() => setNotification(null)}
/>
)}
<TextInput
data-modal-primary-focus
labelText={translations('modalLabelText')}
<FilterableMultiSelect
id="tags-filterable-multiselect"
titleText={translations('modalLabelText')}
placeholder={translations('modalPlaceholderText')}
value={newTagInput}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewTagInput(e.target.value)}
onKeyDown={handleKeyDown}
items={filterableItems}
initialSelectedItems={initialSelectedItems}
itemToString={(item: DisplayedTagType | null) => (item ? item.label : '')}
selectionFeedback="top"
selectedItems={initialSelectedItems}
onChange={({ selectedItems }: { selectedItems: DisplayedTagType[] }) => {
handleFilterableMultiSelectChange(selectedItems);
}}
onInputValueChange={(inputValue: string) => {
setFilterInput(inputValue);
}}
className={styles.tagsTextInput}
/>
<RenderTags
tags={Array.from(stagedTags)}
tags={Array.from(stagedTags).sort()}
isDismissible={true}
size="lg"
onTagRemove={handleTagRemove}
Expand Down
1 change: 0 additions & 1 deletion galasa-ui/src/styles/test-runs/TestRunsPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,3 @@
inset: 0;
z-index: 1;
}

2 changes: 1 addition & 1 deletion galasa-ui/src/styles/test-runs/TestRunsSearch.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
bottom: 5.5rem;
right: 1rem;
z-index: 9999;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,20 @@
.tagsTextInput {
margin-bottom: 1.5rem;
}

/* Uses :global() to access Carbon's global class */
.tagsEditModal :global(.cds--modal-container) {
min-height: 25rem;
max-height: 35rem;
height: 45vh;
}

.tagsEditModal :global(#tags-filterable-multiselect__menu) {
max-height: 18vh;
}

.tagsEditModal :global(.cds--tag.cds--tag--filter.cds--tag--high-contrast) {
visibility: collapse;
width: 0px;
margin: 0px;
}
Loading