123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- // eslint-disable-next-line simple-import-sort/imports
- import {
- filterTypeConfig,
- interchangeableFilterOperators,
- TermOperator,
- Token,
- TokenResult,
- } from 'sentry/components/searchSyntax/parser';
- import {
- IconArrow,
- IconClock,
- IconDelete,
- IconExclamation,
- IconStar,
- IconTag,
- IconToggle,
- IconUser,
- } from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {FieldValueKind} from 'sentry/views/eventsV2/table/types';
- import {ItemType, SearchGroup, SearchItem, Shortcut, ShortcutType} from './types';
- import {Tag} from 'sentry/types';
- export function addSpace(query = '') {
- if (query.length !== 0 && query[query.length - 1] !== ' ') {
- return query + ' ';
- }
- return query;
- }
- export function removeSpace(query = '') {
- if (query[query.length - 1] === ' ') {
- return query.slice(0, query.length - 1);
- }
- return query;
- }
- /**
- * Given a query, and the current cursor position, return the string-delimiting
- * index of the search term designated by the cursor.
- */
- export function getLastTermIndex(query: string, cursor: number) {
- // TODO: work with quoted-terms
- const cursorOffset = query.slice(cursor).search(/\s|$/);
- return cursor + (cursorOffset === -1 ? 0 : cursorOffset);
- }
- /**
- * Returns an array of query terms, including incomplete terms
- *
- * e.g. ["is:unassigned", "browser:\"Chrome 33.0\"", "assigned"]
- */
- export function getQueryTerms(query: string, cursor: number) {
- return query.slice(0, cursor).match(/\S+:"[^"]*"?|\S+/g);
- }
- function getTitleForType(type: ItemType) {
- if (type === ItemType.TAG_VALUE) {
- return t('Values');
- }
- if (type === ItemType.RECENT_SEARCH) {
- return t('Recent Searches');
- }
- if (type === ItemType.DEFAULT) {
- return t('Common Search Terms');
- }
- if (type === ItemType.TAG_OPERATOR) {
- return t('Operator Helpers');
- }
- if (type === ItemType.PROPERTY) {
- return t('Properties');
- }
- return t('Keys');
- }
- function getIconForTypeAndTag(type: ItemType, tagName: string) {
- if (type === ItemType.RECENT_SEARCH) {
- return <IconClock size="xs" />;
- }
- if (type === ItemType.DEFAULT) {
- return <IconStar size="xs" />;
- }
- // Change based on tagName and default to "icon-tag"
- switch (tagName) {
- case 'is':
- return <IconToggle size="xs" />;
- case 'assigned':
- case 'bookmarks':
- return <IconUser size="xs" />;
- case 'firstSeen':
- case 'lastSeen':
- case 'event.timestamp':
- return <IconClock size="xs" />;
- default:
- return <IconTag size="xs" />;
- }
- }
- const filterSearchItems = (
- searchItems: SearchItem[],
- recentSearchItems?: SearchItem[],
- maxSearchItems?: number,
- queryCharsLeft?: number
- ) => {
- if (maxSearchItems && maxSearchItems > 0) {
- searchItems = searchItems.filter(
- (value: SearchItem, index: number) =>
- index < maxSearchItems || value.ignoreMaxSearchItems
- );
- }
- if (queryCharsLeft || queryCharsLeft === 0) {
- searchItems = searchItems.flatMap(item => {
- if (!item.children) {
- if (!item.value || item.value.length <= queryCharsLeft) {
- return [item];
- }
- return [];
- }
- const newItem = {
- ...item,
- children: item.children.filter(
- child => !child.value || child.value.length <= queryCharsLeft
- ),
- };
- if (newItem.children.length === 0) {
- return [];
- }
- return [newItem];
- });
- searchItems = searchItems.filter(
- (value: SearchItem) => !value.value || value.value.length <= queryCharsLeft
- );
- if (recentSearchItems) {
- recentSearchItems = recentSearchItems.filter(
- (value: SearchItem) => !value.value || value.value.length <= queryCharsLeft
- );
- }
- }
- return {searchItems, recentSearchItems};
- };
- export function createSearchGroups(
- searchItems: SearchItem[],
- recentSearchItems: SearchItem[] | undefined,
- tagName: string,
- type: ItemType,
- maxSearchItems?: number,
- queryCharsLeft?: number,
- isDefaultState?: boolean
- ) {
- const activeSearchItem = 0;
- const {searchItems: filteredSearchItems, recentSearchItems: filteredRecentSearchItems} =
- filterSearchItems(searchItems, recentSearchItems, maxSearchItems, queryCharsLeft);
- const searchGroup: SearchGroup = {
- title: getTitleForType(type),
- type: type === ItemType.INVALID_TAG ? type : 'header',
- icon: getIconForTypeAndTag(type, tagName),
- children: [...filteredSearchItems],
- };
- const recentSearchGroup: SearchGroup | undefined =
- filteredRecentSearchItems && filteredRecentSearchItems.length > 0
- ? {
- title: t('Recent Searches'),
- type: 'header',
- icon: <IconClock size="xs" />,
- children: [...filteredRecentSearchItems],
- }
- : undefined;
- if (searchGroup.children && !!searchGroup.children.length) {
- searchGroup.children[activeSearchItem] = {
- ...searchGroup.children[activeSearchItem],
- };
- }
- const flatSearchItems = filteredSearchItems.flatMap(item => {
- if (item.children) {
- if (!item.value) {
- return [...item.children];
- }
- return [item, ...item.children];
- }
- return [item];
- });
- if (isDefaultState) {
- // Recent searches first in default state.
- return {
- searchGroups: [...(recentSearchGroup ? [recentSearchGroup] : []), searchGroup],
- flatSearchItems: [
- ...(recentSearchItems ? recentSearchItems : []),
- ...flatSearchItems,
- ],
- activeSearchItem: -1,
- };
- }
- return {
- searchGroups: [searchGroup, ...(recentSearchGroup ? [recentSearchGroup] : [])],
- flatSearchItems: [
- ...flatSearchItems,
- ...(recentSearchItems ? recentSearchItems : []),
- ],
- activeSearchItem: -1,
- };
- }
- export function generateOperatorEntryMap(tag: string) {
- return {
- [TermOperator.Default]: {
- type: ItemType.TAG_OPERATOR,
- value: ':',
- desc: `${tag}:${t('[value]')}`,
- documentation: 'is equal to',
- },
- [TermOperator.GreaterThanEqual]: {
- type: ItemType.TAG_OPERATOR,
- value: ':>=',
- desc: `${tag}:${t('>=[value]')}`,
- documentation: 'is greater than or equal to',
- },
- [TermOperator.LessThanEqual]: {
- type: ItemType.TAG_OPERATOR,
- value: ':<=',
- desc: `${tag}:${t('<=[value]')}`,
- documentation: 'is less than or equal to',
- },
- [TermOperator.GreaterThan]: {
- type: ItemType.TAG_OPERATOR,
- value: ':>',
- desc: `${tag}:${t('>[value]')}`,
- documentation: 'is greater than',
- },
- [TermOperator.LessThan]: {
- type: ItemType.TAG_OPERATOR,
- value: ':<',
- desc: `${tag}:${t('<[value]')}`,
- documentation: 'is less than',
- },
- [TermOperator.Equal]: {
- type: ItemType.TAG_OPERATOR,
- value: ':=',
- desc: `${tag}:${t('=[value]')}`,
- documentation: 'is equal to',
- },
- [TermOperator.NotEqual]: {
- type: ItemType.TAG_OPERATOR,
- value: '!:',
- desc: `!${tag}:${t('[value]')}`,
- documentation: 'is not equal to',
- },
- };
- }
- export function getValidOps(
- filterToken: TokenResult<Token.Filter>
- ): readonly TermOperator[] {
- // If the token is invalid we want to use the possible expected types as our filter type
- const validTypes = filterToken.invalid?.expectedType ?? [filterToken.filter];
- // Determine any interchangeable filter types for our valid types
- const interchangeableTypes = validTypes.map(
- type => interchangeableFilterOperators[type] ?? []
- );
- // Combine all types
- const allValidTypes = [...new Set([...validTypes, ...interchangeableTypes.flat()])];
- // Find all valid operations
- const validOps = new Set<TermOperator>(
- allValidTypes.map(type => filterTypeConfig[type].validOps).flat()
- );
- return [...validOps];
- }
- export const shortcuts: Shortcut[] = [
- {
- text: 'Delete',
- shortcutType: ShortcutType.Delete,
- hotkeys: {
- actual: 'option+backspace',
- },
- icon: <IconDelete size="xs" color="gray300" />,
- canRunShortcut: token => {
- return token?.type === Token.Filter;
- },
- },
- {
- text: 'Exclude',
- shortcutType: ShortcutType.Negate,
- hotkeys: {
- actual: 'option+1',
- },
- icon: <IconExclamation size="xs" color="gray300" />,
- canRunShortcut: token => {
- return token?.type === Token.Filter && !token.negated;
- },
- },
- {
- text: 'Include',
- shortcutType: ShortcutType.Negate,
- hotkeys: {
- actual: 'option+1',
- },
- icon: <IconExclamation size="xs" color="gray300" />,
- canRunShortcut: token => {
- return token?.type === Token.Filter && token.negated;
- },
- },
- {
- text: 'Previous',
- shortcutType: ShortcutType.Previous,
- hotkeys: {
- actual: 'option+left',
- },
- icon: <IconArrow direction="left" size="xs" color="gray300" />,
- canRunShortcut: (token, count) => {
- return count > 1 || (count > 0 && token?.type !== Token.Filter);
- },
- },
- {
- text: 'Next',
- shortcutType: ShortcutType.Next,
- hotkeys: {
- actual: 'option+right',
- },
- icon: <IconArrow direction="right" size="xs" color="gray300" />,
- canRunShortcut: (token, count) => {
- return count > 1 || (count > 0 && token?.type !== Token.Filter);
- },
- },
- ];
- /**
- * Groups tag keys based on the "." character in their key.
- * For example, "device.arch" and "device.name" will be grouped together as children of "device", a non-interactive parent.
- * The parent will become interactive if there exists a key "device".
- */
- export const getTagItemsFromKeys = (
- tagKeys: string[],
- supportedTags: {
- [key: string]: Tag;
- },
- getFieldDoc?: (key: string) => React.ReactNode
- ) => {
- return [...tagKeys]
- .sort((a, b) => a.localeCompare(b))
- .reduce((groups, key) => {
- const keyWithColon = `${key}:`;
- const sections = key.split('.');
- const kind = supportedTags[key]?.kind;
- const documentation = getFieldDoc?.(key) || '-';
- const item: SearchItem = {
- value: keyWithColon,
- title: key,
- documentation,
- kind,
- };
- const lastGroup = groups.at(-1);
- const [title] = sections;
- if (kind !== FieldValueKind.FUNCTION && lastGroup) {
- if (lastGroup.children && lastGroup.title === title) {
- lastGroup.children.push(item);
- return groups;
- }
- if (lastGroup.title && lastGroup.title.split('.')[0] === title) {
- if (lastGroup.title === title) {
- return [
- ...groups.slice(0, -1),
- {
- title,
- value: lastGroup.value,
- documentation: lastGroup.documentation,
- kind: lastGroup.kind,
- children: [item],
- },
- ];
- }
- // Add a blank parent if the last group's full key is not the same as the title
- return [
- ...groups.slice(0, -1),
- {
- title,
- value: null,
- documentation: '-',
- kind: lastGroup.kind,
- children: [lastGroup, item],
- },
- ];
- }
- }
- return [...groups, item];
- }, [] as SearchItem[]);
- };
- /**
- * Sets an item as active within a search group array and returns new search groups without mutating.
- * the item is compared via value, so this function assumes that each value is unique.
- */
- export const getSearchGroupWithItemMarkedActive = (
- searchGroups: SearchGroup[],
- currentItem: SearchItem,
- active: boolean
- ) => {
- return searchGroups.map(group => ({
- ...group,
- children: group.children?.map(item => {
- if (item.value === currentItem.value) {
- return {
- ...item,
- active,
- };
- }
- if (item.children && item.children.length > 0) {
- return {
- ...item,
- children: item.children.map(child => {
- if (child.value === currentItem.value) {
- return {
- ...child,
- active,
- };
- }
- return child;
- }),
- };
- }
- return item;
- }),
- }));
- };
|