(null);\n\n return (\n \n {\n setupAutocompleteFn(editor, monaco);\n\n const updateElementHeight = () => {\n const containerDiv = containerRef.current;\n if (containerDiv !== null) {\n const pixelHeight = editor.getContentHeight();\n containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;\n containerDiv.style.width = '100%';\n const pixelWidth = containerDiv.clientWidth;\n editor.layout({ width: pixelWidth, height: pixelHeight });\n }\n };\n\n editor.onDidContentSizeChange(updateElementHeight);\n updateElementHeight();\n\n editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {\n onRunQueryRef.current(editor.getValue());\n });\n }}\n />\n
\n );\n}\n\n// this number was chosen by testing various values. it might be necessary\n// because of the width of the border, not sure.\n//it needs to do 2 things:\n// 1. when the editor is single-line, it should make the editor height be visually correct\n// 2. when the editor is multi-line, the editor should not be \"scrollable\" (meaning,\n// you do a scroll-movement in the editor, and it will scroll the content by a couple pixels\n// up & down. this we want to avoid)\nconst EDITOR_HEIGHT_OFFSET = 2;\n\n/**\n * Hook that returns function that will set up monaco autocomplete for the label selector\n * @param datasource\n */\nfunction useAutocomplete(datasource: ParcaDataSource) {\n const autocompleteDisposeFun = useRef<(() => void) | null>(null);\n useEffect(() => {\n // when we unmount, we unregister the autocomplete-function, if it was registered\n return () => {\n autocompleteDisposeFun.current?.();\n };\n }, []);\n\n // This should be run in monaco onEditorDidMount\n return async (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {\n const provider = new CompletionProvider(datasource, monaco, editor);\n await provider.init();\n const { dispose } = monaco.languages.registerCompletionItemProvider(langId, provider);\n autocompleteDisposeFun.current = dispose;\n };\n}\n\n// we must only run the setup code once\nlet parcaqlSetupDone = false;\nconst langId = 'parca';\n\nfunction ensureParcaQL(monaco: Monaco) {\n if (parcaqlSetupDone === false) {\n parcaqlSetupDone = true;\n const { aliases, extensions, mimetypes, def } = languageDefinition;\n monaco.languages.register({ id: langId, aliases, extensions, mimetypes });\n monaco.languages.setMonarchTokensProvider(langId, def.language);\n monaco.languages.setLanguageConfiguration(langId, def.languageConfiguration);\n }\n}\n\nconst getStyles = () => {\n return {\n queryField: css`\n flex: 1;\n // Not exactly sure but without this the editor doe not shrink after resizing (so you can make it bigger but not\n // smaller). At the same time this does not actually make the editor 100px because it has flex 1 so I assume\n // this should sort of act as a flex-basis (but flex-basis does not work for this). So yeah CSS magic.\n width: 100px;\n `,\n wrapper: css`\n display: flex;\n flex: 1;\n border: 1px solid rgba(36, 41, 46, 0.3);\n border-radius: 2px;\n `,\n };\n};\n","import { css, cx } from '@emotion/css';\nimport React from 'react';\nimport { useToggle } from 'react-use';\n\nimport { CoreApp, GrafanaTheme2 } from '@grafana/data';\nimport { Icon, useStyles2, RadioButtonGroup, Field, clearButtonStyles, Button } from '@grafana/ui';\n\nimport { Query } from '../types';\n\nimport { Stack } from './Stack';\n\nexport interface Props {\n query: Query;\n onQueryTypeChange: (val: Query['queryType']) => void;\n app?: CoreApp;\n}\n\nconst rangeOptions: Array<{ value: Query['queryType']; label: string; description: string }> = [\n { value: 'metrics', label: 'Metric', description: 'Return aggregated metrics' },\n { value: 'profile', label: 'Profile', description: 'Return profile' },\n { value: 'both', label: 'Both', description: 'Return both metric and profile data' },\n];\n\nfunction getOptions(app?: CoreApp) {\n if (app === CoreApp.Explore) {\n return rangeOptions;\n }\n return rangeOptions.filter((option) => option.value !== 'both');\n}\n\n/**\n * Base on QueryOptionGroup component from grafana/ui but that is not available yet.\n */\nexport function QueryOptions({ query, onQueryTypeChange, app }: Props) {\n const [isOpen, toggleOpen] = useToggle(false);\n const styles = useStyles2(getStyles);\n const options = getOptions(app);\n const buttonStyles = useStyles2(clearButtonStyles);\n\n return (\n \n \n {isOpen && (\n \n \n \n \n
\n )}\n \n );\n}\n\nconst getStyles = (theme: GrafanaTheme2) => {\n return {\n switchLabel: css({\n color: theme.colors.text.secondary,\n cursor: 'pointer',\n fontSize: theme.typography.bodySmall.fontSize,\n '&:hover': {\n color: theme.colors.text.primary,\n },\n }),\n header: css({\n display: 'flex',\n cursor: 'pointer',\n alignItems: 'baseline',\n color: theme.colors.text.primary,\n '&:hover': {\n background: theme.colors.emphasize(theme.colors.background.primary, 0.03),\n },\n }),\n title: css({\n flexGrow: 1,\n overflow: 'hidden',\n fontSize: theme.typography.bodySmall.fontSize,\n fontWeight: theme.typography.fontWeightMedium,\n margin: 0,\n }),\n description: css({\n color: theme.colors.text.secondary,\n fontSize: theme.typography.bodySmall.fontSize,\n paddingLeft: theme.spacing(2),\n gap: theme.spacing(2),\n display: 'flex',\n }),\n body: css({\n display: 'flex',\n paddingTop: theme.spacing(2),\n gap: theme.spacing(2),\n flexWrap: 'wrap',\n }),\n toggle: css({\n color: theme.colors.text.secondary,\n marginRight: `${theme.spacing(1)}`,\n }),\n };\n};\n","import { defaults } from 'lodash';\nimport React, { useMemo, useState } from 'react';\nimport { useMount } from 'react-use';\n\nimport { CoreApp, QueryEditorProps } from '@grafana/data';\nimport { ButtonCascader, CascaderOption } from '@grafana/ui';\n\nimport { defaultParca, defaultParcaQueryType, Parca } from '../dataquery.gen';\nimport { ParcaDataSource } from '../datasource';\nimport { ParcaDataSourceOptions, ProfileTypeMessage, Query } from '../types';\n\nimport { EditorRow } from './EditorRow';\nimport { EditorRows } from './EditorRows';\nimport { LabelsEditor } from './LabelsEditor';\nimport { QueryOptions } from './QueryOptions';\n\nexport type Props = QueryEditorProps;\n\nexport const defaultQuery: Partial = {\n ...defaultParca,\n queryType: defaultParcaQueryType,\n};\n\nexport function QueryEditor(props: Props) {\n const [profileTypes, setProfileTypes] = useState([]);\n\n function onProfileTypeChange(value: string[], selectedOptions: CascaderOption[]) {\n if (selectedOptions.length === 0) {\n return;\n }\n const id = selectedOptions[selectedOptions.length - 1].value;\n if (typeof id !== 'string') {\n throw new Error('id is not string');\n }\n props.onChange({ ...props.query, profileTypeId: id });\n }\n\n function onLabelSelectorChange(value: string) {\n props.onChange({ ...props.query, labelSelector: value });\n }\n\n function handleRunQuery(value: string) {\n props.onChange({ ...props.query, labelSelector: value });\n props.onRunQuery();\n }\n\n useMount(async () => {\n const profileTypes = await props.datasource.getProfileTypes();\n setProfileTypes(profileTypes);\n });\n\n // Turn profileTypes into cascader options\n const cascaderOptions = useMemo(() => {\n let mainTypes = new Map();\n // Classify profile types by name then sample type.\n for (let profileType of profileTypes) {\n if (!mainTypes.has(profileType.name)) {\n mainTypes.set(profileType.name, {\n label: profileType.name,\n value: profileType.ID,\n children: [],\n });\n }\n mainTypes.get(profileType.name)?.children?.push({\n label: profileType.sample_type,\n value: profileType.ID,\n });\n }\n return Array.from(mainTypes.values());\n }, [profileTypes]);\n\n const selectedProfileName = useMemo(() => {\n if (!profileTypes) {\n return 'Loading';\n }\n const profile = profileTypes.find((type) => type.ID === props.query.profileTypeId);\n if (!profile) {\n return 'Select a profile type';\n }\n\n return profile.name + ' - ' + profile.sample_type;\n }, [props.query.profileTypeId, profileTypes]);\n\n let query = normalizeQuery(props.query, props.app);\n\n return (\n \n \n \n {selectedProfileName}\n \n \n \n \n {\n props.onChange({ ...query, queryType: val });\n }}\n app={props.app}\n />\n \n \n );\n}\n\nfunction normalizeQuery(query: Query, app?: CoreApp) {\n let normalized = defaults(query, defaultQuery);\n if (app !== CoreApp.Explore && normalized.queryType === 'both') {\n // In dashboards and other places, we can't show both types of graphs at the same time.\n // This will also be a default when having 'both' query and adding it from explore to dashboard\n normalized.queryType = 'profile';\n }\n return normalized;\n}\n","import { Observable, of } from 'rxjs';\n\nimport { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data';\nimport { DataSourceWithBackend } from '@grafana/runtime';\n\nimport { ParcaDataSourceOptions, Query, ProfileTypeMessage } from './types';\n\nexport class ParcaDataSource extends DataSourceWithBackend {\n constructor(instanceSettings: DataSourceInstanceSettings) {\n super(instanceSettings);\n }\n\n query(request: DataQueryRequest): Observable {\n if (!request.targets.every((q) => q.profileTypeId)) {\n // When changing data source in explore, firs query can be sent without filled in profileTypeId\n return of({ data: [] });\n }\n\n return super.query(request);\n }\n\n async getProfileTypes(): Promise {\n return await super.getResource('profileTypes');\n }\n\n async getLabelNames(): Promise {\n return await super.getResource('labelNames');\n }\n\n async getLabelValues(labelName: string): Promise {\n return await super.getResource('labelValues', { label: labelName });\n }\n}\n","import { DataSourcePlugin } from '@grafana/data';\n\nimport { ConfigEditor } from './ConfigEditor';\nimport { QueryEditor } from './QueryEditor/QueryEditor';\nimport { ParcaDataSource } from './datasource';\nimport { Query, ParcaDataSourceOptions } from './types';\n\nexport const plugin = new DataSourcePlugin(ParcaDataSource)\n .setConfigEditor(ConfigEditor)\n .setQueryEditor(QueryEditor);\n","import { useRef } from 'react';\nvar useLatest = function (value) {\n var ref = useRef(value);\n ref.current = value;\n return ref;\n};\nexport default useLatest;\n","import useEffectOnce from './useEffectOnce';\nvar useMount = function (fn) {\n useEffectOnce(function () {\n fn();\n });\n};\nexport default useMount;\n"],"names":["Stack","props","styles","getStyles","theme","ConfigEditor","options","onOptionsChange","DataSourceHttpSettings","config","defaultParcaQueryType","defaultParca","EditorRow","children","stackProps","EditorRows","languageDefinition","CompletionProvider","datasource","monaco","editor","names","acc","name","model","position","range","offset","getRangeAndOffset","situation","getSituation","completionItems","maxIndexDigits","item","index","getMonacoCompletionItemKind","key","values","val","type","labelNameRegex","labelValueRegex","labelPairsRegex","inLabelValueRegex","inLabelNameRegex","text","matches","existingLabels","match","_","value","matchLabelValue","word","positionClone","LabelsEditor","setupAutocompleteFn","useAutocomplete","onRunQueryRef","useLatest","containerRef","CodeEditor","langId","ensureParcaQL","updateElementHeight","containerDiv","pixelHeight","EDITOR_HEIGHT_OFFSET","pixelWidth","autocompleteDisposeFun","provider","dispose","parcaqlSetupDone","aliases","extensions","mimetypes","def","rangeOptions","getOptions","app","option","QueryOptions","query","onQueryTypeChange","isOpen","toggleOpen","useToggle","buttonStyles","Button","Icon","Field","RadioButtonGroup","defaultQuery","QueryEditor","profileTypes","setProfileTypes","onProfileTypeChange","selectedOptions","id","onLabelSelectorChange","handleRunQuery","useMount","cascaderOptions","mainTypes","profileType","selectedProfileName","profile","normalizeQuery","ButtonCascader","normalized","ParcaDataSource","DataSourceWithBackend","instanceSettings","request","q","of","labelName","ref","fn"],"sourceRoot":""}