renderer.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import space from 'app/styles/space';
  5. import {ParseResult, Token, TokenResult} from './parser';
  6. class ResultRenderer {
  7. renderFilter = (filter: TokenResult<Token.Filter>) => (
  8. <FilterToken>
  9. {filter.negated && <Negation>!</Negation>}
  10. {this.renderKey(filter.key, filter.negated)}
  11. {filter.operator && <Operator>{filter.operator}</Operator>}
  12. <Value>{this.renderToken(filter.value)}</Value>
  13. </FilterToken>
  14. );
  15. renderKey = (
  16. key: TokenResult<Token.KeySimple | Token.KeyAggregate | Token.KeyExplicitTag>,
  17. negated?: boolean
  18. ) => {
  19. let value: React.ReactNode = key.text;
  20. if (key.type === Token.KeyExplicitTag) {
  21. value = (
  22. <ExplicitKey prefix={key.prefix}>
  23. {key.key.quoted ? `"${key.key.value}"` : key.key.value}
  24. </ExplicitKey>
  25. );
  26. }
  27. return <Key negated={!!negated}>{value}:</Key>;
  28. };
  29. renderList = (token: TokenResult<Token.ValueNumberList | Token.ValueTextList>) => (
  30. <InList>
  31. {token.items.map(({value, separator}) => [
  32. <ListComma key="comma">{separator}</ListComma>,
  33. this.renderToken(value),
  34. ])}
  35. </InList>
  36. );
  37. renderNumber = (token: TokenResult<Token.ValueNumber>) => (
  38. <Fragment>
  39. {token.value}
  40. <Unit>{token.unit}</Unit>
  41. </Fragment>
  42. );
  43. renderToken = (token: TokenResult<Token>) => {
  44. switch (token.type) {
  45. case Token.Spaces:
  46. return token.value;
  47. case Token.Filter:
  48. return this.renderFilter(token);
  49. case Token.LogicGroup:
  50. return <LogicGroup>{this.renderResult(token.inner)}</LogicGroup>;
  51. case Token.LogicBoolean:
  52. return <LogicBoolean>{token.value}</LogicBoolean>;
  53. case Token.ValueBoolean:
  54. return <Boolean>{token.text}</Boolean>;
  55. case Token.ValueIso8601Date:
  56. return <DateTime>{token.text}</DateTime>;
  57. case Token.ValueTextList:
  58. case Token.ValueNumberList:
  59. return this.renderList(token);
  60. case Token.ValueNumber:
  61. return this.renderNumber(token);
  62. default:
  63. return token.text;
  64. }
  65. };
  66. renderResult = (result: ParseResult) =>
  67. result
  68. .map(this.renderToken)
  69. .map((renderedToken, i) => <Fragment key={i}>{renderedToken}</Fragment>);
  70. }
  71. const renderer = new ResultRenderer();
  72. type Props = {
  73. /**
  74. * The result from parsing the search query string
  75. */
  76. parsedQuery: ParseResult;
  77. /**
  78. * The current location of the cursror within the query. This is used to
  79. * highligh active tokens and trigger error tooltips.
  80. */
  81. cursorPosition?: number;
  82. };
  83. /**
  84. * Renders the parsed query with syntax highlighting.
  85. */
  86. export default function HighlightQuery({parsedQuery}: Props) {
  87. const rendered = renderer.renderResult(parsedQuery);
  88. return <Fragment>{rendered}</Fragment>;
  89. }
  90. const FilterToken = styled('span')`
  91. --token-bg: ${p => p.theme.searchTokenBackground};
  92. --token-border: ${p => p.theme.searchTokenBorder};
  93. --token-value-color: ${p => p.theme.blue300};
  94. `;
  95. const filterCss = css`
  96. background: var(--token-bg);
  97. border: 0.5px solid var(--token-border);
  98. padding: ${space(0.25)} 0;
  99. `;
  100. const Negation = styled('span')`
  101. ${filterCss};
  102. border-right: none;
  103. padding-left: 1px;
  104. margin-left: -2px;
  105. font-weight: bold;
  106. border-radius: 2px 0 0 2px;
  107. color: ${p => p.theme.red300};
  108. `;
  109. const Key = styled('span')<{negated: boolean}>`
  110. ${filterCss};
  111. border-right: none;
  112. font-weight: bold;
  113. ${p =>
  114. !p.negated
  115. ? css`
  116. border-radius: 2px 0 0 2px;
  117. padding-left: 1px;
  118. margin-left: -2px;
  119. `
  120. : css`
  121. border-left: none;
  122. margin-left: 0;
  123. `};
  124. `;
  125. const ExplicitKey = styled('span')<{prefix: string}>`
  126. &:before,
  127. &:after {
  128. color: ${p => p.theme.subText};
  129. }
  130. &:before {
  131. content: '${p => p.prefix}[';
  132. }
  133. &:after {
  134. content: ']';
  135. }
  136. `;
  137. const Operator = styled('span')`
  138. ${filterCss};
  139. border-left: none;
  140. border-right: none;
  141. margin: -1px 0;
  142. color: ${p => p.theme.orange400};
  143. `;
  144. const Value = styled('span')`
  145. ${filterCss};
  146. border-left: none;
  147. border-radius: 0 2px 2px 0;
  148. color: var(--token-value-color);
  149. margin: -1px -2px -1px 0;
  150. padding-right: 1px;
  151. `;
  152. const Unit = styled('span')`
  153. font-weight: bold;
  154. color: ${p => p.theme.green300};
  155. `;
  156. const LogicBoolean = styled('span')`
  157. font-weight: bold;
  158. color: ${p => p.theme.red300};
  159. `;
  160. const Boolean = styled('span')`
  161. color: ${p => p.theme.pink300};
  162. `;
  163. const DateTime = styled('span')`
  164. color: ${p => p.theme.green300};
  165. `;
  166. const ListComma = styled('span')`
  167. color: ${p => p.theme.gray300};
  168. `;
  169. const InList = styled('span')`
  170. &:before {
  171. content: '[';
  172. font-weight: bold;
  173. color: ${p => p.theme.purple300};
  174. }
  175. &:after {
  176. content: ']';
  177. font-weight: bold;
  178. color: ${p => p.theme.purple300};
  179. }
  180. ${Value} {
  181. color: ${p => p.theme.purple300};
  182. }
  183. `;
  184. const LogicGroup = styled('span')`
  185. &:before,
  186. &:after {
  187. position: relative;
  188. font-weight: bold;
  189. color: ${p => p.theme.white};
  190. padding: 3px 0;
  191. background: ${p => p.theme.red200};
  192. border-radius: 1px;
  193. }
  194. &:before {
  195. left: -3px;
  196. content: '(';
  197. }
  198. &:after {
  199. right: -3px;
  200. content: ')';
  201. }
  202. `;