Skip to content
Open
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
2 changes: 1 addition & 1 deletion galasa-ui/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@
"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",
Expand Down
2 changes: 1 addition & 1 deletion galasa-ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
"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",
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: [],
}
}
}
94 changes: 64 additions & 30 deletions galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ 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 { updateRunTags, getExistingTagObjects } from '@/actions/runsAction';

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

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

const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState<boolean>(false);
const [newTagInput, setNewTagInput] = useState<string>('');
const [filterInput, setFilterInput] = useState<string>('');
const [stagedTags, setStagedTags] = useState<Set<string>>(new Set(tags));
const [notification, setNotification] = useState<{
kind: 'success' | 'error';
Expand All @@ -41,6 +41,23 @@ 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();
setExistingTagNames(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 +85,44 @@ 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: { id: string; text: string }[]) => {
// Update staged tags based on selected items
const newStagedTags = new Set(selectedItems.map(item => item.text));
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 getFilterableItems = () => {
const items: { id: string; text: string }[] = [];

// Add existing tags from the system
existingTagNames.forEach((tagName, index) => {
items.push({
id: `existing-tag-${index}`,
text: tagName,
});
});

// Clear the input after staging
setNewTagInput('');
// Add the current filter input as an option if it's not empty and not already in the list
if (filterInput.trim() && !items.some(item => item.text === filterInput.trim())) {
items.push({
id: 'custom-input',
text: filterInput.trim(),
});
}

return items;
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleStageNewTags();
}
// Get initially selected items based on staged tags
const getInitialSelectedItems = () => {
const items = getFilterableItems();
return items.filter(item => stagedTags.has(item.text));
};

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

Expand Down Expand Up @@ -206,13 +233,20 @@ 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={getFilterableItems()}
initialSelectedItems={getInitialSelectedItems()}
itemToString={(item: { id: string; text: string } | null) => (item ? item.text : '')}
selectionFeedback="top-after-reopen"
onChange={({ selectedItems }: { selectedItems: { id: string; text: string }[] }) => {
handleFilterableMultiSelectChange(selectedItems);
}}
onInputValueChange={(inputValue: string) => {
setFilterInput(inputValue);
}}
className={styles.tagsTextInput}
/>
<RenderTags
Expand Down
Loading