mirror of
https://github.com/grafana/grafana.git
synced 2025-08-14 07:00:47 +08:00

* 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>
234 lines
6.6 KiB
TypeScript
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',
|
|
}),
|
|
};
|
|
};
|