Files
grafana/public/app/features/query/components/QueryEditorRowHeader.tsx
Jack Westbrook f96e4e9ad2 Frontend: Remove Angular (#99760)
* chore(angularsupport): delete feature toggle to disable angular

* feat(angular-support): remove config.angularSupportEnabled

* chore(jest): remove angular from setup file

* chore(angular): delete angular deprecation ui components

* refactor(angular): move migration featureflags into migration notice

* chore(dashboard): remove angular deprecation notices

* chore(annotations): remove angular editor loader

* feat(appwrapper): no more angular app loading

* feat(pluginscatalog): clean up angular plugin warnings and logic

* chore(angular): delete angular app and associated files

* feat(plugins): delete old angular graph plugin

* feat(plugins): delete old angular table panel

* feat(frontend): remove unused appEvent type

* feat(dashboards): clean up angular from panel options and menu

* feat(plugins): remove graph and table-old from built in plugins and delete sdk

* feat(frontend): remove angular related imports in routes and explore graph

* feat(theme): remove angular panel styles from global styles

* chore(i18n): run make i18n-extract

* test(api_plugins_test): refresh snapshot due to deleting old graph and table plugins

* chore(angulardeprecation): delete angular migration notice components and usage

* test(frontend): clean up tests that assert rendering angular deprecation notices

* chore(backend): remove autoMigrateOldPanels feature flag

* chore(config): remove angularSupportEnabled from config preventing loading angular plugins

* chore(graphpanel): remove autoMigrateGraphPanel from feature toggles

* chore(tablepanel): delete autoMigrateTablePanel feature flag

* chore(piechart): delete autoMigratePiechartPanel feature flag

* chore(worldmappanel): remove autoMigrateWorldmapPanel feature toggle

* chore(statpanel): remove autoMigrateStatPanel feature flag

* feat(dashboards): remove automigrate feature flags and always auto migrate angular panels

* test(pluginsintegration): fix failing loader test

* test(frontend): wip: fix failures and skip erroring migration tests

* chore(codeowners): remove deleted angular related files and directories

* test(graphite): remove angular mock from test file

* test(dashboards): skip failing exporter test, remove angularSupportEnabled flags

* test(dashbaord): skip another failing panel menu test

* Tests: fixes pkg/services/pluginsintegration/loader/loader_test.go (#100505)

* Tests: fixes pkg/services/pluginsintegration/plugins_integration_test.go

* Trigger Build

* chore(dashboards): remove angularComponent from getPanelMenu, update test

* feat(dashboards): remove all usage of AngularComponent and getAngularLoader

* chore(betterer): refresh results file

* feat(plugins): remove PluginAngularBadge component and usage

* feat(datasource_srv): remove usage of getLegacyAngularInjector

* feat(queryeditor): delete AngularQueryComponentScope type

* Chore: removes Angular from plugin_loader

* Chore: remove angular from getPlugin

* Chore: fix i18n

* Trigger Build

* Chore: remove more Angular from importPanelPlugin

* Chore: remove search options warning

* Chore: remove and deprecate Angular related

* chore(angular): remove angular dependencies from core and runtime

* chore(runtime): delete angular injector

* chore(data): delete angular scope from event bus

* chore(plugin-catalog): remove code pushing app plugins angular config page

* chore(yarn): refresh lock file

* chore(frontend): remove ng-loader from webpack configs, remove systemjs cjs plugin

* chore(navigation): remove tether-drop cleanup from GrafanaRouter, delete dependency

* chore(runtime): delete AngularLoader

* chore(betterer): refresh results file

* chore(betterer): fix out of sync results file

* feat(query): fix type and import errors in QueryEditorRow

* test(dashboards): delete skipped angular related tests

* Tests: add back tests and fix betterer

* Tests: fix broken test

* Trigger build

* chore(i18n): remove angular deprecation related strings

* test: clean up connections and plugins catalog tests

* chore(betterer): update results file

---------

Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
2025-04-04 11:31:35 +02:00

234 lines
6.6 KiB
TypeScript

import { css, cx } from '@emotion/css';
import * as React from 'react';
import { ReactNode, useState } from 'react';
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { FieldValidationMessage, Icon, Input, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
export interface Props<TQuery extends DataQuery = DataQuery> {
query: TQuery;
queries: TQuery[];
hidden?: boolean;
dataSource: DataSourceInstanceSettings;
renderExtras?: () => ReactNode;
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
onChange: (query: TQuery) => void;
collapsedText: string | null;
alerting?: boolean;
hideRefId?: boolean;
}
export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQuery>) => {
const { query, queries, onChange, collapsedText, renderExtras, hidden, hideRefId = false } = props;
const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [validationError, setValidationError] = useState<string | null>(null);
const onEditQuery = (event: React.SyntheticEvent) => {
setIsEditing(true);
};
const onEndEditName = (newName: string) => {
setIsEditing(false);
// Ignore change if invalid
if (validationError) {
setValidationError(null);
return;
}
if (query.refId !== newName) {
onChange({
...query,
refId: newName,
});
}
};
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newName = event.currentTarget.value.trim();
if (newName.length === 0) {
setValidationError('An empty query name is not allowed');
return;
}
for (const otherQuery of queries) {
if (otherQuery !== query && newName === otherQuery.refId) {
setValidationError('Query name already exists');
return;
}
}
if (validationError) {
setValidationError(null);
}
};
const onEditQueryBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
onEndEditName(event.currentTarget.value.trim());
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onEndEditName(event.currentTarget.value);
}
};
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
return (
<>
<div className={styles.wrapper}>
{!hideRefId && !isEditing && (
<button
className={styles.queryNameWrapper}
aria-label={selectors.components.QueryEditorRow.title(query.refId)}
title={t('query.query-editor-row-header.query-name-div-title-edit-query-name', 'Edit query name')}
onClick={onEditQuery}
data-testid="query-name-div"
type="button"
>
<span className={styles.queryName}>{query.refId}</span>
<Icon name="pen" className={styles.queryEditIcon} size="sm" />
</button>
)}
{!hideRefId && isEditing && (
<>
<Input
type="text"
defaultValue={query.refId}
onBlur={onEditQueryBlur}
autoFocus
onKeyDown={onKeyDown}
onFocus={onFocus}
invalid={validationError !== null}
onChange={onInputChange}
className={styles.queryNameInput}
data-testid="query-name-input"
/>
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
</>
)}
{renderDataSource(props, styles)}
{renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
{hidden && (
<em className={styles.contextInfo}>
<Trans i18nKey="query.query-editor-row-header.hidden">Hidden</Trans>
</em>
)}
</div>
{collapsedText && <div className={styles.collapsedText}>{collapsedText}</div>}
</>
);
};
const renderDataSource = <TQuery extends DataQuery>(
props: Props<TQuery>,
styles: ReturnType<typeof getStyles>
): ReactNode => {
const { alerting, dataSource, onChangeDataSource } = props;
if (!onChangeDataSource) {
return <em className={styles.contextInfo}>({dataSource.name})</em>;
}
return (
<div className={styles.itemWrapper}>
<DataSourcePicker
dashboard={true}
variables={true}
alerting={alerting}
current={dataSource.name}
onChange={onChangeDataSource}
/>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
label: 'Wrapper',
display: 'flex',
alignItems: 'center',
marginLeft: theme.spacing(0.5),
overflow: 'hidden',
}),
queryNameWrapper: css({
display: 'flex',
cursor: 'pointer',
border: '1px solid transparent',
borderRadius: theme.shape.radius.default,
alignItems: 'center',
padding: theme.spacing(0, 0, 0, 0.5),
margin: 0,
background: 'transparent',
overflow: 'hidden',
'&:hover': {
background: theme.colors.action.hover,
border: `1px dashed ${theme.colors.border.strong}`,
},
'&:focus': {
border: `2px solid ${theme.colors.primary.border}`,
},
'&:hover, &:focus': {
'.query-name-edit-icon': {
visibility: 'visible',
},
},
}),
queryName: css({
fontWeight: theme.typography.fontWeightMedium,
color: theme.colors.primary.text,
cursor: 'pointer',
overflow: 'hidden',
marginLeft: theme.spacing(0.5),
}),
queryEditIcon: cx(
css({
marginLeft: theme.spacing(2),
visibility: 'hidden',
}),
'query-name-edit-icon'
),
queryNameInput: css({
maxWidth: '300px',
margin: '-4px 0',
}),
collapsedText: css({
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
paddingLeft: theme.spacing(1),
alignItems: 'center',
overflow: 'hidden',
fontStyle: 'italic',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}),
contextInfo: css({
fontSize: theme.typography.bodySmall.fontSize,
fontStyle: 'italic',
color: theme.colors.text.secondary,
paddingLeft: '10px',
paddingRight: '10px',
}),
itemWrapper: css({
display: 'flex',
marginLeft: '4px',
}),
};
};