Chore: Convert to functional components (#112951)
* refactor ErrorBoundary so it doesn't trigger the lint rule * refactor ErrorBoundaryAlert to functional component * convert StatPanel to a functional component * convert ServiceAccountPicker to a functional component * convert UserPicker to a functional component * don't need displayName when not memoized * convert TimelineChart to a functional component * convert UserLdapSyncInfo to a functional component * convert UserOrgs to functional component * convert OrgRow to a functional component * convert UserSessions to a functional component * convert TimePickerSettings to a functional component * convert DataSourcePluginSettings to a functional component * convert ExploreTimeControls to a functional component * convert SearchBarInput to a functional component * convert LiveConnectionWarning to a functional component * convert ConcatenateTransformerEditor * convert ConstantVariableEditor a functional component * convert VariableInput to a functional component * convert ConfigEditor to a functional component * convert CSVWavesEditor to a functional component
This commit is contained in:
@@ -539,11 +539,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"packages/grafana-sql/src/components/ErrorBoundary.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
@@ -666,11 +661,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/FormField/FormField.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -1278,21 +1268,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/Select/ServiceAccountPicker.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/Select/TeamPicker.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/Select/UserPicker.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/SharedPreferences/SharedPreferences.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 8
|
||||
@@ -1342,11 +1322,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/core/components/TimelineChart/TimelineChart.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/config.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
@@ -1483,25 +1458,17 @@
|
||||
"public/app/features/admin/UserLdapSyncInfo.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/admin/UserOrgs.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/features/admin/UserProfile.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/admin/UserSessions.tsx": {
|
||||
"public/app/features/admin/UserProfile.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
@@ -2399,9 +2366,6 @@
|
||||
"public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 5
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx": {
|
||||
@@ -2752,11 +2716,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/datasources/components/DataSourcePluginSettings.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/datasources/components/DataSourcePluginState.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
@@ -2872,11 +2831,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/ExploreTimeControls.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/Logs/LiveLogs.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
@@ -2977,11 +2931,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/common/SearchBarInput.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/demo/trace-generators.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -3118,11 +3067,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/live/LiveConnectionWarning.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/live/centrifuge/LiveDataStream.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -3578,11 +3522,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -3700,11 +3639,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/variables/constant/ConstantVariableEditor.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/variables/constant/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -3787,9 +3721,6 @@
|
||||
"public/app/features/variables/pickers/shared/VariableInput.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/variables/pickers/shared/VariableOptions.tsx": {
|
||||
@@ -4004,11 +3935,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -4207,11 +4133,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
@@ -4920,11 +4841,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/stat/StatPanel.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/state-timeline/migrations.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { Component, ErrorInfo, PropsWithChildren } from 'react';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
|
||||
@@ -6,14 +6,14 @@ type Props = {
|
||||
fallBackComponent?: React.ReactNode;
|
||||
};
|
||||
|
||||
export class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>, { hasError: boolean }> {
|
||||
constructor(props: React.PropsWithChildren<Props>) {
|
||||
export class ErrorBoundary extends Component<React.PropsWithChildren<Props>, { hasError: boolean }> {
|
||||
constructor(props: PropsWithChildren<Props>) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.setState({ hasError: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PureComponent, ReactNode, ComponentType, ErrorInfo } from 'react';
|
||||
import { PureComponent, ReactNode, ComponentType, ErrorInfo, memo } from 'react';
|
||||
|
||||
import { faro } from '@grafana/faro-web-sdk';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { Alert } from '../Alert/Alert';
|
||||
|
||||
@@ -116,15 +117,9 @@ export interface ErrorBoundaryAlertProps {
|
||||
errorLogger?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
|
||||
static defaultProps: Partial<ErrorBoundaryAlertProps> = {
|
||||
title: 'An unexpected error happened',
|
||||
style: 'alertbox',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, children, style, dependencies, errorLogger, boundaryName } = this.props;
|
||||
|
||||
export const ErrorBoundaryAlert = memo(
|
||||
({ title, children, style = 'alertbox', dependencies, errorLogger, boundaryName }: ErrorBoundaryAlertProps) => {
|
||||
const alertTitle = title ?? t('grafana-ui.error-boundary.title', 'An unexpected error happened');
|
||||
return (
|
||||
<ErrorBoundary dependencies={dependencies} errorLogger={errorLogger} boundaryName={boundaryName}>
|
||||
{({ error, errorInfo }) => {
|
||||
@@ -134,7 +129,7 @@ export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
|
||||
|
||||
if (style === 'alertbox') {
|
||||
return (
|
||||
<Alert title={title || ''}>
|
||||
<Alert title={alertTitle}>
|
||||
<details style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{error && error.toString()}
|
||||
<br />
|
||||
@@ -144,12 +139,14 @@ export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
|
||||
);
|
||||
}
|
||||
|
||||
return <ErrorWithStack title={title || ''} error={error} errorInfo={errorInfo} />;
|
||||
return <ErrorWithStack title={alertTitle} error={error} errorInfo={errorInfo} />;
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ErrorBoundaryAlert.displayName = 'ErrorBoundaryAlert';
|
||||
|
||||
/**
|
||||
* HOC for wrapping a component in an error boundary.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import debounce from 'debounce-promise';
|
||||
import { isNil } from 'lodash';
|
||||
import { Component } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -14,66 +14,58 @@ export interface Props {
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isLoading: boolean;
|
||||
}
|
||||
export const ServiceAccountPicker = ({ className, onSelected, inputId }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
export class ServiceAccountPicker extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { isLoading: false };
|
||||
}
|
||||
const search = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async (query?: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
search = debounce(
|
||||
async (query?: string) => {
|
||||
this.setState({ isLoading: true });
|
||||
if (isNil(query)) {
|
||||
query = '';
|
||||
}
|
||||
|
||||
if (isNil(query)) {
|
||||
query = '';
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.get(`/api/serviceaccounts/search?query=${query}&perpage=100`)
|
||||
.then((result: ServiceAccountsState) => {
|
||||
return result.serviceAccounts.map((sa) => ({
|
||||
id: sa.id,
|
||||
uid: sa.uid,
|
||||
value: sa,
|
||||
label: sa.login,
|
||||
imgUrl: sa.avatarUrl,
|
||||
login: sa.login,
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
},
|
||||
300,
|
||||
{ leading: true }
|
||||
return getBackendSrv()
|
||||
.get(`/api/serviceaccounts/search?query=${query}&perpage=100`)
|
||||
.then((result: ServiceAccountsState) => {
|
||||
return result.serviceAccounts.map((sa) => ({
|
||||
id: sa.id,
|
||||
uid: sa.uid,
|
||||
value: sa,
|
||||
label: sa.login,
|
||||
imgUrl: sa.avatarUrl,
|
||||
login: sa.login,
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
300,
|
||||
{ leading: true }
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
render() {
|
||||
const { className, onSelected, inputId } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="service-account-picker" data-testid="serviceAccountPicker">
|
||||
<AsyncSelect
|
||||
isClearable
|
||||
className={className}
|
||||
inputId={inputId}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={true}
|
||||
loadOptions={this.search}
|
||||
onChange={onSelected}
|
||||
placeholder={t('service-account-picker.select-placeholder', 'Start typing to search for service accounts')}
|
||||
noOptionsMessage={t(
|
||||
'service-account-picker.noOptionsMessage-no-service-accounts-found',
|
||||
'No service accounts found'
|
||||
)}
|
||||
aria-label={t('service-account-picker.select-aria-label', 'Service account picker')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="service-account-picker" data-testid="serviceAccountPicker">
|
||||
<AsyncSelect
|
||||
isClearable
|
||||
className={className}
|
||||
inputId={inputId}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={true}
|
||||
loadOptions={search}
|
||||
onChange={onSelected}
|
||||
placeholder={t('service-account-picker.select-placeholder', 'Start typing to search for service accounts')}
|
||||
noOptionsMessage={t(
|
||||
'service-account-picker.noOptionsMessage-no-service-accounts-found',
|
||||
'No service accounts found'
|
||||
)}
|
||||
aria-label={t('service-account-picker.select-aria-label', 'Service account picker')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import debounce from 'debounce-promise';
|
||||
import { isNil } from 'lodash';
|
||||
import { Component } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -14,63 +14,55 @@ export interface Props {
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isLoading: boolean;
|
||||
}
|
||||
export const UserPicker = ({ className, onSelected, inputId }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
export class UserPicker extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { isLoading: false };
|
||||
}
|
||||
const search = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async (query?: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
search = debounce(
|
||||
async (query?: string) => {
|
||||
this.setState({ isLoading: true });
|
||||
if (isNil(query)) {
|
||||
query = '';
|
||||
}
|
||||
|
||||
if (isNil(query)) {
|
||||
query = '';
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.get(`/api/org/users/lookup?query=${query}&limit=100`)
|
||||
.then((result: OrgUser[]) => {
|
||||
return result.map((user) => ({
|
||||
id: user.userId,
|
||||
uid: user.uid,
|
||||
value: user,
|
||||
label: user.login,
|
||||
imgUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
},
|
||||
300,
|
||||
{ leading: true }
|
||||
return getBackendSrv()
|
||||
.get(`/api/org/users/lookup?query=${query}&limit=100`)
|
||||
.then((result: OrgUser[]) => {
|
||||
return result.map((user) => ({
|
||||
id: user.userId,
|
||||
uid: user.uid,
|
||||
value: user,
|
||||
label: user.login,
|
||||
imgUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
300,
|
||||
{ leading: true }
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
render() {
|
||||
const { className, onSelected, inputId } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="user-picker" data-testid="userPicker">
|
||||
<AsyncSelect
|
||||
isClearable
|
||||
className={className}
|
||||
inputId={inputId}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={true}
|
||||
loadOptions={this.search}
|
||||
onChange={onSelected}
|
||||
placeholder={t('user-picker.select-placeholder', 'Start typing to search for user')}
|
||||
noOptionsMessage={t('user-picker.noOptionsMessage-no-users-found', 'No users found')}
|
||||
aria-label={t('user-picker.select-aria-label', 'User picker')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="user-picker" data-testid="userPicker">
|
||||
<AsyncSelect
|
||||
isClearable
|
||||
className={className}
|
||||
inputId={inputId}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={true}
|
||||
loadOptions={search}
|
||||
onChange={onSelected}
|
||||
placeholder={t('user-picker.select-placeholder', 'Start typing to search for user')}
|
||||
noOptionsMessage={t('user-picker.noOptionsMessage-no-users-found', 'No users found')}
|
||||
aria-label={t('user-picker.select-aria-label', 'User picker')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
|
||||
import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema';
|
||||
@@ -25,69 +25,76 @@ export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsT
|
||||
|
||||
const propsToDiff = ['rowHeight', 'colWidth', 'showValue', 'mergeValues', 'alignValue', 'tooltip', 'paginationRev'];
|
||||
|
||||
export class TimelineChart extends Component<TimelineProps> {
|
||||
getValueColor = (frameIdx: number, fieldIdx: number, value: unknown) => {
|
||||
const field = this.props.frames[frameIdx]?.fields[fieldIdx];
|
||||
export const TimelineChart = (props: TimelineProps) => {
|
||||
const { frames, timeZone, rowHeight, tooltip, legend, legendItems } = props;
|
||||
|
||||
if (field?.display) {
|
||||
const disp = field.display(value); // will apply color modes
|
||||
if (disp.color) {
|
||||
return disp.color;
|
||||
const getValueColor = useCallback(
|
||||
(frameIdx: number, fieldIdx: number, value: unknown) => {
|
||||
const field = frames[frameIdx]?.fields[fieldIdx];
|
||||
|
||||
if (field?.display) {
|
||||
const disp = field.display(value); // will apply color modes
|
||||
if (disp.color) {
|
||||
return disp.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALLBACK_COLOR;
|
||||
};
|
||||
return FALLBACK_COLOR;
|
||||
},
|
||||
[frames]
|
||||
);
|
||||
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
allFrames: this.props.frames,
|
||||
...this.props,
|
||||
const prepConfig = useCallback(
|
||||
(alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
allFrames: frames,
|
||||
...props,
|
||||
|
||||
// Ensure timezones is passed as an array
|
||||
timeZones: Array.isArray(this.props.timeZone) ? this.props.timeZone : [this.props.timeZone],
|
||||
// Ensure timezones is passed as an array
|
||||
timeZones: Array.isArray(timeZone) ? timeZone : [timeZone],
|
||||
|
||||
// When there is only one row, use the full space
|
||||
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
||||
getValueColor: this.getValueColor,
|
||||
// When there is only one row, use the full space
|
||||
rowHeight: alignedFrame.fields.length > 2 ? rowHeight : 1,
|
||||
getValueColor: getValueColor,
|
||||
|
||||
hoverMulti: this.props.tooltip?.mode === TooltipDisplayMode.Multi,
|
||||
});
|
||||
};
|
||||
hoverMulti: tooltip?.mode === TooltipDisplayMode.Multi,
|
||||
});
|
||||
},
|
||||
[frames, props, timeZone, rowHeight, getValueColor, tooltip]
|
||||
);
|
||||
|
||||
renderLegend = (config: UPlotConfigBuilder) => {
|
||||
const { legend, legendItems } = this.props;
|
||||
const renderLegend = useCallback(
|
||||
(config: UPlotConfigBuilder) => {
|
||||
if (!config || !legendItems || !legend || legend.showLegend === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!config || !legendItems || !legend || legend.showLegend === false) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<VizLayout.Legend placement={legend.placement}>
|
||||
<VizLegend placement={legend.placement} items={legendItems} displayMode={legend.displayMode} readonly />
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
},
|
||||
[legend, legendItems]
|
||||
);
|
||||
|
||||
return (
|
||||
<VizLayout.Legend placement={legend.placement}>
|
||||
<VizLegend placement={legend.placement} items={legendItems} displayMode={legend.displayMode} readonly />
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<GraphNG
|
||||
{...this.props}
|
||||
fields={{
|
||||
x: (f) => f.type === FieldType.time,
|
||||
y: (f) =>
|
||||
f.type === FieldType.number ||
|
||||
f.type === FieldType.boolean ||
|
||||
f.type === FieldType.string ||
|
||||
f.type === FieldType.enum,
|
||||
}}
|
||||
prepConfig={this.prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
renderLegend={this.renderLegend}
|
||||
omitHideFromViz={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<GraphNG
|
||||
{...props}
|
||||
fields={{
|
||||
x: (f) => f.type === FieldType.time,
|
||||
y: (f) =>
|
||||
f.type === FieldType.number ||
|
||||
f.type === FieldType.boolean ||
|
||||
f.type === FieldType.string ||
|
||||
f.type === FieldType.enum,
|
||||
}}
|
||||
prepConfig={prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
renderLegend={renderLegend}
|
||||
omitHideFromViz={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
@@ -16,80 +16,72 @@ interface Props {
|
||||
onUserSync: () => void;
|
||||
}
|
||||
|
||||
interface State {}
|
||||
|
||||
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||
const debugLDAPMappingBaseURL = '/admin/authentication/ldap';
|
||||
|
||||
export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
||||
onUserSync = () => {
|
||||
this.props.onUserSync();
|
||||
};
|
||||
export const UserLdapSyncInfo = memo(({ ldapSyncInfo, user, onUserSync }: Props) => {
|
||||
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
||||
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
||||
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`;
|
||||
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
||||
const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync);
|
||||
|
||||
render() {
|
||||
const { ldapSyncInfo, user } = this.props;
|
||||
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
||||
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
||||
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`;
|
||||
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
||||
const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.ldap-sync.title">LDAP Synchronisation</Trans>
|
||||
</h3>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.external-sync-label">External sync</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.external-sync-description">
|
||||
User synced via LDAP. Some changes must be done in LDAP or mappings.
|
||||
</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
label={t('admin.user-ldap-sync-info.label-ldap', 'LDAP')}
|
||||
removeIcon={false}
|
||||
count={0}
|
||||
onClick={undefined}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.next-sync-label">Next scheduled synchronization</Trans>
|
||||
</td>
|
||||
<td colSpan={2}>
|
||||
{ldapSyncInfo.enabled ? (
|
||||
nextSyncTime
|
||||
) : (
|
||||
<Trans i18nKey="admin.ldap-sync.not-enabled">Not enabled</Trans>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="gf-form-button-row">
|
||||
{canSyncLDAPUser && (
|
||||
<Button variant="secondary" onClick={this.onUserSync}>
|
||||
<Trans i18nKey="admin.ldap-sync.sync-button">Sync user</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{canReadLDAPUser && (
|
||||
<LinkButton variant="secondary" href={debugLDAPMappingURL}>
|
||||
<Trans i18nKey="admin.ldap-sync.debug-button">Debug LDAP Mapping</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.ldap-sync.title">LDAP Synchronisation</Trans>
|
||||
</h3>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.external-sync-label">External sync</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.external-sync-description">
|
||||
User synced via LDAP. Some changes must be done in LDAP or mappings.
|
||||
</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
label={t('admin.user-ldap-sync-info.label-ldap', 'LDAP')}
|
||||
removeIcon={false}
|
||||
count={0}
|
||||
onClick={undefined}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans i18nKey="admin.ldap-sync.next-sync-label">Next scheduled synchronization</Trans>
|
||||
</td>
|
||||
<td colSpan={2}>
|
||||
{ldapSyncInfo.enabled ? (
|
||||
nextSyncTime
|
||||
) : (
|
||||
<Trans i18nKey="admin.ldap-sync.not-enabled">Not enabled</Trans>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div className="gf-form-button-row">
|
||||
{canSyncLDAPUser && (
|
||||
<Button variant="secondary" onClick={onUserSync}>
|
||||
<Trans i18nKey="admin.ldap-sync.sync-button">Sync user</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{canReadLDAPUser && (
|
||||
<LinkButton variant="secondary" href={debugLDAPMappingURL}>
|
||||
<Trans i18nKey="admin.ldap-sync.debug-button">Debug LDAP Mapping</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
UserLdapSyncInfo.displayName = 'UserLdapSyncInfo';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { createRef, PureComponent, ReactElement } from 'react';
|
||||
import { memo, PureComponent, ReactElement, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
Icon,
|
||||
Modal,
|
||||
stylesFactory,
|
||||
Themeable2,
|
||||
Tooltip,
|
||||
useStyles2,
|
||||
withTheme2,
|
||||
Stack,
|
||||
TextLink,
|
||||
} from '@grafana/ui';
|
||||
@@ -37,73 +35,63 @@ interface Props {
|
||||
onOrgAdd: (orgId: number, role: OrgRole) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showAddOrgModal: boolean;
|
||||
}
|
||||
export const UserOrgs = memo(({ user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd }: Props) => {
|
||||
const [showAddOrgModal, setShowAddOrgModal] = useState(false);
|
||||
const addToOrgButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
export class UserOrgs extends PureComponent<Props, State> {
|
||||
addToOrgButtonRef = createRef<HTMLButtonElement>();
|
||||
state = {
|
||||
showAddOrgModal: false,
|
||||
const showOrgAddModal = () => {
|
||||
setShowAddOrgModal(true);
|
||||
};
|
||||
|
||||
showOrgAddModal = () => {
|
||||
this.setState({ showAddOrgModal: true });
|
||||
const dismissOrgAddModal = () => {
|
||||
setShowAddOrgModal(false);
|
||||
addToOrgButtonRef.current?.focus();
|
||||
};
|
||||
|
||||
dismissOrgAddModal = () => {
|
||||
this.setState({ showAddOrgModal: false }, () => {
|
||||
this.addToOrgButtonRef.current?.focus();
|
||||
});
|
||||
};
|
||||
const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser;
|
||||
|
||||
render() {
|
||||
const { user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props;
|
||||
const { showAddOrgModal } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.user-orgs.title">Organizations</Trans>
|
||||
</h3>
|
||||
<Stack gap={1.5} direction="column">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
{orgs.map((org, index) => (
|
||||
<OrgRow
|
||||
key={`${org.orgId}-${index}`}
|
||||
isExternalUser={isExternalUser}
|
||||
user={user}
|
||||
org={org}
|
||||
onOrgRoleChange={onOrgRoleChange}
|
||||
onOrgRemove={onOrgRemove}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser;
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.user-orgs.title">Organizations</Trans>
|
||||
</h3>
|
||||
<Stack gap={1.5} direction="column">
|
||||
<table className="filter-table form-inline">
|
||||
<tbody>
|
||||
{orgs.map((org, index) => (
|
||||
<OrgRow
|
||||
key={`${org.orgId}-${index}`}
|
||||
isExternalUser={isExternalUser}
|
||||
user={user}
|
||||
org={org}
|
||||
onOrgRoleChange={onOrgRoleChange}
|
||||
onOrgRemove={onOrgRemove}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
{canAddToOrg && (
|
||||
<Button variant="secondary" onClick={showOrgAddModal} ref={addToOrgButtonRef}>
|
||||
<Trans i18nKey="admin.user-orgs.add-button">Add user to organization</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AddToOrgModal
|
||||
user={user}
|
||||
userOrgs={orgs}
|
||||
isOpen={showAddOrgModal}
|
||||
onOrgAdd={onOrgAdd}
|
||||
onDismiss={dismissOrgAddModal}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
UserOrgs.displayName = 'UserOrgs';
|
||||
|
||||
<div>
|
||||
{canAddToOrg && (
|
||||
<Button variant="secondary" onClick={this.showOrgAddModal} ref={this.addToOrgButtonRef}>
|
||||
<Trans i18nKey="admin.user-orgs.add-button">Add user to organization</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AddToOrgModal
|
||||
user={user}
|
||||
userOrgs={orgs}
|
||||
isOpen={showAddOrgModal}
|
||||
onOrgAdd={onOrgAdd}
|
||||
onDismiss={this.dismissOrgAddModal}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
const getOrgRowStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
removeButton: css({
|
||||
marginRight: '0.6rem',
|
||||
@@ -130,9 +118,9 @@ const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
interface OrgRowProps extends Themeable2 {
|
||||
interface OrgRowProps {
|
||||
user?: UserDTO;
|
||||
org: UserOrg;
|
||||
isExternalUser?: boolean;
|
||||
@@ -140,124 +128,116 @@ interface OrgRowProps extends Themeable2 {
|
||||
onOrgRoleChange: (orgId: number, newRole: OrgRole) => void;
|
||||
}
|
||||
|
||||
class UnThemedOrgRow extends PureComponent<OrgRowProps> {
|
||||
state = {
|
||||
currentRole: this.props.org.role,
|
||||
isChangingRole: false,
|
||||
roleOptions: [],
|
||||
};
|
||||
const OrgRow = memo(({ user, org, isExternalUser, onOrgRemove, onOrgRoleChange }: OrgRowProps) => {
|
||||
const [currentRole, setCurrentRole] = useState(org.role);
|
||||
const [isChangingRole, setIsChangingRole] = useState(false);
|
||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||
const styles = useStyles2(getOrgRowStyles);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
||||
fetchRoleOptions(this.props.org.orgId)
|
||||
.then((roles) => this.setState({ roleOptions: roles }))
|
||||
fetchRoleOptions(org.orgId)
|
||||
.then((roles) => setRoleOptions(roles))
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [org.orgId]);
|
||||
|
||||
onOrgRemove = async () => {
|
||||
const { org } = this.props;
|
||||
this.props.onOrgRemove(org.orgId);
|
||||
const handleOrgRemove = async () => {
|
||||
onOrgRemove(org.orgId);
|
||||
};
|
||||
|
||||
onChangeRoleClick = () => {
|
||||
const { org } = this.props;
|
||||
this.setState({ isChangingRole: true, currentRole: org.role });
|
||||
const handleChangeRoleClick = () => {
|
||||
setIsChangingRole(true);
|
||||
setCurrentRole(org.role);
|
||||
};
|
||||
|
||||
onOrgRoleChange = (newRole: OrgRole) => {
|
||||
this.setState({ currentRole: newRole });
|
||||
const handleOrgRoleChange = (newRole: OrgRole) => {
|
||||
setCurrentRole(newRole);
|
||||
};
|
||||
|
||||
onOrgRoleSave = () => {
|
||||
this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole);
|
||||
const handleOrgRoleSave = () => {
|
||||
onOrgRoleChange(org.orgId, currentRole);
|
||||
};
|
||||
|
||||
onCancelClick = () => {
|
||||
this.setState({ isChangingRole: false });
|
||||
const handleCancelClick = () => {
|
||||
setIsChangingRole(false);
|
||||
};
|
||||
|
||||
onBasicRoleChange = (newRole: OrgRole) => {
|
||||
this.props.onOrgRoleChange(this.props.org.orgId, newRole);
|
||||
const handleBasicRoleChange = (newRole: OrgRole) => {
|
||||
onOrgRoleChange(org.orgId, newRole);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { user, org, isExternalUser, theme } = this.props;
|
||||
const authSource = user?.authLabels?.length && user?.authLabels[0];
|
||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||
const { currentRole, isChangingRole } = this.state;
|
||||
const styles = getOrgRowStyles(theme);
|
||||
const labelClass = cx('width-16', styles.label);
|
||||
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite);
|
||||
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser;
|
||||
const rolePickerDisabled = isExternalUser || !canChangeRole;
|
||||
const authSource = user?.authLabels?.length && user?.authLabels[0];
|
||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||
const labelClass = cx('width-16', styles.label);
|
||||
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite);
|
||||
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser;
|
||||
const rolePickerDisabled = isExternalUser || !canChangeRole;
|
||||
|
||||
const inputId = `${org.name}-input`;
|
||||
return (
|
||||
<tr>
|
||||
<td className={labelClass}>
|
||||
<label htmlFor={inputId}>{org.name}</label>
|
||||
</td>
|
||||
{contextSrv.licensedAccessControlEnabled() ? (
|
||||
<td>
|
||||
<div className={styles.rolePickerWrapper}>
|
||||
<div className={styles.rolePicker}>
|
||||
<UserRolePicker
|
||||
userId={user?.id || 0}
|
||||
orgId={org.orgId}
|
||||
basicRole={org.role}
|
||||
roleOptions={this.state.roleOptions}
|
||||
onBasicRoleChange={this.onBasicRoleChange}
|
||||
basicRoleDisabled={rolePickerDisabled}
|
||||
basicRoleDisabledMessage="This user's role is not editable because it is synchronized from your auth provider.
|
||||
Refer to the Grafana authentication docs for details."
|
||||
/>
|
||||
</div>
|
||||
{isExternalUser && <ExternalUserTooltip lockMessage={lockMessage} />}
|
||||
const inputId = `${org.name}-input`;
|
||||
return (
|
||||
<tr>
|
||||
<td className={labelClass}>
|
||||
<label htmlFor={inputId}>{org.name}</label>
|
||||
</td>
|
||||
{contextSrv.licensedAccessControlEnabled() ? (
|
||||
<td>
|
||||
<div className={styles.rolePickerWrapper}>
|
||||
<div className={styles.rolePicker}>
|
||||
<UserRolePicker
|
||||
userId={user?.id || 0}
|
||||
orgId={org.orgId}
|
||||
basicRole={org.role}
|
||||
roleOptions={roleOptions}
|
||||
onBasicRoleChange={handleBasicRoleChange}
|
||||
basicRoleDisabled={rolePickerDisabled}
|
||||
basicRoleDisabledMessage="This user's role is not editable because it is synchronized from your auth provider.
|
||||
Refer to the Grafana authentication docs for details."
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<>
|
||||
{isChangingRole ? (
|
||||
<td>
|
||||
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} autoFocus />
|
||||
</td>
|
||||
) : (
|
||||
<td className="width-25">{org.role}</td>
|
||||
)}
|
||||
<td colSpan={1}>
|
||||
{canChangeRole && (
|
||||
<ChangeOrgButton
|
||||
lockMessage={lockMessage}
|
||||
isExternalUser={isExternalUser}
|
||||
onChangeRoleClick={this.onChangeRoleClick}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onOrgRoleSave={this.onOrgRoleSave}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
<td colSpan={1}>
|
||||
{canRemoveFromOrg && (
|
||||
<ConfirmButton
|
||||
confirmText={t('admin.un-themed-org-row.confirmText-confirm-removal', 'Confirm removal')}
|
||||
confirmVariant="destructive"
|
||||
onCancel={this.onCancelClick}
|
||||
onConfirm={this.onOrgRemove}
|
||||
>
|
||||
{t('admin.user-orgs.remove-button', 'Remove from organization')}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{isExternalUser && <ExternalUserTooltip lockMessage={lockMessage} />}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const OrgRow = withTheme2(UnThemedOrgRow);
|
||||
) : (
|
||||
<>
|
||||
{isChangingRole ? (
|
||||
<td>
|
||||
<OrgRolePicker inputId={inputId} value={currentRole} onChange={handleOrgRoleChange} autoFocus />
|
||||
</td>
|
||||
) : (
|
||||
<td className="width-25">{org.role}</td>
|
||||
)}
|
||||
<td colSpan={1}>
|
||||
{canChangeRole && (
|
||||
<ChangeOrgButton
|
||||
lockMessage={lockMessage}
|
||||
isExternalUser={isExternalUser}
|
||||
onChangeRoleClick={handleChangeRoleClick}
|
||||
onCancelClick={handleCancelClick}
|
||||
onOrgRoleSave={handleOrgRoleSave}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
<td colSpan={1}>
|
||||
{canRemoveFromOrg && (
|
||||
<ConfirmButton
|
||||
confirmText={t('admin.un-themed-org-row.confirmText-confirm-removal', 'Confirm removal')}
|
||||
confirmVariant="destructive"
|
||||
onCancel={handleCancelClick}
|
||||
onConfirm={handleOrgRemove}
|
||||
>
|
||||
{t('admin.user-orgs.remove-button', 'Remove from organization')}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
OrgRow.displayName = 'OrgRow';
|
||||
|
||||
const getAddToOrgModalStyles = stylesFactory(() => ({
|
||||
modal: css({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRef, PureComponent } from 'react';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui';
|
||||
@@ -15,120 +15,107 @@ interface Props {
|
||||
onAllSessionsRevoke: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showLogoutModal: boolean;
|
||||
}
|
||||
export const UserSessions = memo(({ sessions, onSessionRevoke, onAllSessionsRevoke }: Props) => {
|
||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||
const forceAllLogoutButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
class BaseUserSessions extends PureComponent<Props, State> {
|
||||
forceAllLogoutButton = createRef<HTMLButtonElement>();
|
||||
state: State = {
|
||||
showLogoutModal: false,
|
||||
const showLogoutConfirmationModal = () => {
|
||||
setShowLogoutModal(true);
|
||||
};
|
||||
|
||||
showLogoutConfirmationModal = () => {
|
||||
this.setState({ showLogoutModal: true });
|
||||
const dismissLogoutConfirmationModal = () => {
|
||||
setShowLogoutModal(false);
|
||||
forceAllLogoutButton.current?.focus();
|
||||
};
|
||||
|
||||
dismissLogoutConfirmationModal = () => {
|
||||
this.setState({ showLogoutModal: false }, () => {
|
||||
this.forceAllLogoutButton.current?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
onSessionRevoke = (id: number) => {
|
||||
const handleSessionRevoke = (id: number) => {
|
||||
return () => {
|
||||
this.props.onSessionRevoke(id);
|
||||
onSessionRevoke(id);
|
||||
};
|
||||
};
|
||||
|
||||
onAllSessionsRevoke = () => {
|
||||
this.setState({ showLogoutModal: false });
|
||||
this.props.onAllSessionsRevoke();
|
||||
const handleAllSessionsRevoke = () => {
|
||||
setShowLogoutModal(false);
|
||||
onAllSessionsRevoke();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { sessions } = this.props;
|
||||
const { showLogoutModal } = this.state;
|
||||
const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout);
|
||||
|
||||
const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.user-sessions.title">Sessions</Trans>
|
||||
</h3>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
<div>
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.last-seen-column">Last seen</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.logged-on-column">Logged on</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.ip-column">IP address</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.browser-column">Browser and OS</Trans>
|
||||
</th>
|
||||
<th colSpan={2}>
|
||||
<Trans i18nKey="user-session.auth-module-column">Identity Provider</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions &&
|
||||
sessions.map((session, index) => (
|
||||
<tr key={`${session.id}-${index}`}>
|
||||
<td>{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||
<td>
|
||||
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
|
||||
</td>
|
||||
<td>
|
||||
{canLogout && (
|
||||
<ConfirmButton
|
||||
confirmText={t('admin.base-user-sessions.confirmText-confirm-logout', 'Confirm logout')}
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleSessionRevoke(session.id)}
|
||||
>
|
||||
{t('admin.user-sessions.force-logout-button', 'Force logout')}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-heading">
|
||||
<Trans i18nKey="admin.user-sessions.title">Sessions</Trans>
|
||||
</h3>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
<div>
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.last-seen-column">Last seen</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.logged-on-column">Logged on</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.ip-column">IP address</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="admin.user-sessions.browser-column">Browser and OS</Trans>
|
||||
</th>
|
||||
<th colSpan={2}>
|
||||
<Trans i18nKey="user-session.auth-module-column">Identity Provider</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions &&
|
||||
sessions.map((session, index) => (
|
||||
<tr key={`${session.id}-${index}`}>
|
||||
<td>{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||
<td>
|
||||
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
|
||||
</td>
|
||||
<td>
|
||||
{canLogout && (
|
||||
<ConfirmButton
|
||||
confirmText={t('admin.base-user-sessions.confirmText-confirm-logout', 'Confirm logout')}
|
||||
confirmVariant="destructive"
|
||||
onConfirm={this.onSessionRevoke(session.id)}
|
||||
>
|
||||
{t('admin.user-sessions.force-logout-button', 'Force logout')}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{canLogout && sessions.length > 0 && (
|
||||
<Button variant="secondary" onClick={this.showLogoutConfirmationModal} ref={this.forceAllLogoutButton}>
|
||||
<Trans i18nKey="admin.user-sessions.force-logout-all-button">Force logout from all devices</Trans>
|
||||
</Button>
|
||||
<div>
|
||||
{canLogout && sessions.length > 0 && (
|
||||
<Button variant="secondary" onClick={showLogoutConfirmationModal} ref={forceAllLogoutButton}>
|
||||
<Trans i18nKey="admin.user-sessions.force-logout-all-button">Force logout from all devices</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={showLogoutModal}
|
||||
title={t('admin.base-user-sessions.title-force-logout-from-all-devices', 'Force logout from all devices')}
|
||||
body={t(
|
||||
'admin.base-user-sessions.body-force-logout-from-all-devices',
|
||||
'Are you sure you want to force logout from all devices?'
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={showLogoutModal}
|
||||
title={t('admin.base-user-sessions.title-force-logout-from-all-devices', 'Force logout from all devices')}
|
||||
body={t(
|
||||
'admin.base-user-sessions.body-force-logout-from-all-devices',
|
||||
'Are you sure you want to force logout from all devices?'
|
||||
)}
|
||||
confirmText={t('admin.base-user-sessions.confirmText-force-logout', 'Force logout')}
|
||||
onConfirm={this.onAllSessionsRevoke}
|
||||
onDismiss={this.dismissLogoutConfirmationModal}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const UserSessions = BaseUserSessions;
|
||||
confirmText={t('admin.base-user-sessions.confirmText-force-logout', 'Force logout')}
|
||||
onConfirm={handleAllSessionsRevoke}
|
||||
onDismiss={dismissLogoutConfirmationModal}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
UserSessions.displayName = 'UserSessions';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
import * as React from 'react';
|
||||
import { FormEvent, memo, useState } from 'react';
|
||||
|
||||
import { rangeUtil, TimeZone } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -24,49 +23,58 @@ interface Props {
|
||||
liveNow?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isNowDelayValid: boolean;
|
||||
}
|
||||
export const TimePickerSettings = memo(
|
||||
({
|
||||
onWeekStartChange,
|
||||
onTimeZoneChange,
|
||||
onRefreshIntervalChange,
|
||||
onNowDelayChange,
|
||||
onHideTimePickerChange,
|
||||
onLiveNowChange,
|
||||
refreshIntervals,
|
||||
timePickerHidden,
|
||||
nowDelay,
|
||||
timezone,
|
||||
weekStart,
|
||||
liveNow,
|
||||
}: Props) => {
|
||||
const [isNowDelayValid, setIsNowDelayValid] = useState(true);
|
||||
|
||||
export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
state: State = { isNowDelayValid: true };
|
||||
const handleNowDelayChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
onNowDelayChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value;
|
||||
if (isEmpty(value)) {
|
||||
setIsNowDelayValid(true);
|
||||
return onNowDelayChange(value);
|
||||
}
|
||||
|
||||
if (isEmpty(value)) {
|
||||
this.setState({ isNowDelayValid: true });
|
||||
return this.props.onNowDelayChange(value);
|
||||
}
|
||||
if (rangeUtil.isValidTimeSpan(value)) {
|
||||
setIsNowDelayValid(true);
|
||||
return onNowDelayChange(value);
|
||||
}
|
||||
|
||||
if (rangeUtil.isValidTimeSpan(value)) {
|
||||
this.setState({ isNowDelayValid: true });
|
||||
return this.props.onNowDelayChange(value);
|
||||
}
|
||||
setIsNowDelayValid(false);
|
||||
};
|
||||
|
||||
this.setState({ isNowDelayValid: false });
|
||||
};
|
||||
const handleHideTimePickerChange = () => {
|
||||
onHideTimePickerChange(!timePickerHidden);
|
||||
};
|
||||
|
||||
onHideTimePickerChange = () => {
|
||||
this.props.onHideTimePickerChange(!this.props.timePickerHidden);
|
||||
};
|
||||
const handleLiveNowChange = () => {
|
||||
onLiveNowChange(!liveNow);
|
||||
};
|
||||
|
||||
onLiveNowChange = () => {
|
||||
this.props.onLiveNowChange(!this.props.liveNow);
|
||||
};
|
||||
const handleTimeZoneChange = (timeZone?: string) => {
|
||||
if (typeof timeZone !== 'string') {
|
||||
return;
|
||||
}
|
||||
onTimeZoneChange(timeZone);
|
||||
};
|
||||
|
||||
onTimeZoneChange = (timeZone?: string) => {
|
||||
if (typeof timeZone !== 'string') {
|
||||
return;
|
||||
}
|
||||
this.props.onTimeZoneChange(timeZone);
|
||||
};
|
||||
const handleWeekStartChange = (weekStart?: WeekStart) => {
|
||||
onWeekStartChange(weekStart);
|
||||
};
|
||||
|
||||
onWeekStartChange = (weekStart?: WeekStart) => {
|
||||
this.props.onWeekStartChange(weekStart);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsableSection label={t('dashboard-settings.time-picker.time-options-label', 'Time options')} isOpen={true}>
|
||||
<Field
|
||||
@@ -76,8 +84,8 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
<TimeZonePicker
|
||||
inputId="time-options-input"
|
||||
includeInternal={true}
|
||||
value={this.props.timezone}
|
||||
onChange={this.onTimeZoneChange}
|
||||
value={timezone}
|
||||
onChange={handleTimeZoneChange}
|
||||
width={40}
|
||||
/>
|
||||
</Field>
|
||||
@@ -85,17 +93,9 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
label={t('dashboard-settings.time-picker.week-start-label', 'Week start')}
|
||||
data-testid={selectors.components.WeekStartPicker.containerV2}
|
||||
>
|
||||
<WeekStartPicker
|
||||
inputId="week-start-input"
|
||||
width={40}
|
||||
value={this.props.weekStart}
|
||||
onChange={this.onWeekStartChange}
|
||||
/>
|
||||
<WeekStartPicker inputId="week-start-input" width={40} value={weekStart} onChange={handleWeekStartChange} />
|
||||
</Field>
|
||||
<AutoRefreshIntervals
|
||||
refreshIntervals={this.props.refreshIntervals}
|
||||
onRefreshIntervalChange={this.props.onRefreshIntervalChange}
|
||||
/>
|
||||
<AutoRefreshIntervals refreshIntervals={refreshIntervals} onRefreshIntervalChange={onRefreshIntervalChange} />
|
||||
<Field
|
||||
label={t('dashboard-settings.time-picker.now-delay-label', 'Now delay')}
|
||||
description={t(
|
||||
@@ -105,19 +105,15 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
>
|
||||
<Input
|
||||
id="now-delay-input"
|
||||
invalid={!this.state.isNowDelayValid}
|
||||
invalid={!isNowDelayValid}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="0m"
|
||||
onChange={this.onNowDelayChange}
|
||||
defaultValue={this.props.nowDelay}
|
||||
onChange={handleNowDelayChange}
|
||||
defaultValue={nowDelay}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('dashboard-settings.time-picker.hide-time-picker', 'Hide time picker')}>
|
||||
<Switch
|
||||
id="hide-time-picker-toggle"
|
||||
value={!!this.props.timePickerHidden}
|
||||
onChange={this.onHideTimePickerChange}
|
||||
/>
|
||||
<Switch id="hide-time-picker-toggle" value={!!timePickerHidden} onChange={handleHideTimePickerChange} />
|
||||
</Field>
|
||||
<Field
|
||||
label={t('dashboard-settings.time-picker.refresh-live-dashboards-label', 'Refresh live dashboards')}
|
||||
@@ -126,9 +122,10 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
'Continuously update panels when the time range includes the current time'
|
||||
)}
|
||||
>
|
||||
<Switch id="refresh-live-dashboards-toggle" value={!!this.props.liveNow} onChange={this.onLiveNowChange} />
|
||||
<Switch id="refresh-live-dashboards-toggle" value={!!liveNow} onChange={handleLiveNowChange} />
|
||||
</Field>
|
||||
</CollapsableSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
TimePickerSettings.displayName = 'TimePickerSettings';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createElement, PureComponent } from 'react';
|
||||
import { createElement, memo } from 'react';
|
||||
|
||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||
import { writableProxy } from 'app/features/plugins/extensions/utils';
|
||||
@@ -12,36 +12,23 @@ export interface Props {
|
||||
onModelChange: (dataSource: DataSourceSettings) => void;
|
||||
}
|
||||
|
||||
export class DataSourcePluginSettings extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.onModelChanged = this.onModelChanged.bind(this);
|
||||
export const DataSourcePluginSettings = memo(({ plugin, dataSource, onModelChange }: Props) => {
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
onModelChanged = (dataSource: DataSourceSettings) => {
|
||||
this.props.onModelChange(dataSource);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { plugin, dataSource } = this.props;
|
||||
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{plugin.components.ConfigEditor &&
|
||||
createElement(plugin.components.ConfigEditor, {
|
||||
options: writableProxy(dataSource, {
|
||||
source: 'datasource',
|
||||
pluginId: plugin.meta?.id,
|
||||
pluginVersion: plugin.meta?.info?.version,
|
||||
}),
|
||||
onOptionsChange: this.onModelChanged,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{plugin.components.ConfigEditor &&
|
||||
createElement(plugin.components.ConfigEditor, {
|
||||
options: writableProxy(dataSource, {
|
||||
source: 'datasource',
|
||||
pluginId: plugin.meta?.id,
|
||||
pluginVersion: plugin.meta?.info?.version,
|
||||
}),
|
||||
onOptionsChange: onModelChange,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
DataSourcePluginSettings.displayName = 'DataSourcePluginSettings';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
import { TimeRange, RawTimeRange, dateTimeForTimeZone, dateMath } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
@@ -22,9 +20,19 @@ export interface Props {
|
||||
onChangeFiscalYearStartMonth: (fiscalYearStartMonth: number) => void;
|
||||
}
|
||||
|
||||
export class ExploreTimeControls extends Component<Props> {
|
||||
onMoveTimePicker = (direction: number) => {
|
||||
const { range, onChangeTime, timeZone } = this.props;
|
||||
export const ExploreTimeControls = ({
|
||||
range,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
splitted,
|
||||
syncedTimes,
|
||||
onChangeTimeSync,
|
||||
hideText,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
onChangeTime,
|
||||
}: Props) => {
|
||||
const onMoveTimePicker = (direction: number) => {
|
||||
const { from, to } = getShiftedTimeRange(direction, range);
|
||||
const nextTimeRange = {
|
||||
from: dateTimeForTimeZone(timeZone, from),
|
||||
@@ -34,14 +42,14 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
onChangeTime(nextTimeRange);
|
||||
};
|
||||
|
||||
onMoveForward = () => this.onMoveTimePicker(1);
|
||||
onMoveBack = () => this.onMoveTimePicker(-1);
|
||||
const onMoveForward = () => onMoveTimePicker(1);
|
||||
const onMoveBack = () => onMoveTimePicker(-1);
|
||||
|
||||
onChangeTimePicker = (timeRange: TimeRange) => {
|
||||
const onChangeTimePicker = (timeRange: TimeRange) => {
|
||||
const adjustedFrom = dateMath.isMathString(timeRange.raw.from) ? timeRange.raw.from : timeRange.from;
|
||||
const adjustedTo = dateMath.isMathString(timeRange.raw.to) ? timeRange.raw.to : timeRange.to;
|
||||
|
||||
this.props.onChangeTime({
|
||||
onChangeTime({
|
||||
from: adjustedFrom,
|
||||
to: adjustedTo,
|
||||
});
|
||||
@@ -52,8 +60,7 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
onZoom = () => {
|
||||
const { range, onChangeTime, timeZone } = this.props;
|
||||
const onZoom = () => {
|
||||
const { from, to } = getZoomedTimeRange(range, 2);
|
||||
const nextTimeRange = {
|
||||
from: dateTimeForTimeZone(timeZone, from),
|
||||
@@ -63,40 +70,27 @@ export class ExploreTimeControls extends Component<Props> {
|
||||
onChangeTime(nextTimeRange);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
range,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
splitted,
|
||||
syncedTimes,
|
||||
onChangeTimeSync,
|
||||
hideText,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
} = this.props;
|
||||
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
|
||||
const timePickerCommonProps = {
|
||||
value: range,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
onMoveBackward: this.onMoveBack,
|
||||
onMoveForward: this.onMoveForward,
|
||||
onZoom: this.onZoom,
|
||||
hideText,
|
||||
};
|
||||
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
|
||||
const timePickerCommonProps = {
|
||||
value: range,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
onMoveBackward: onMoveBack,
|
||||
onMoveForward: onMoveForward,
|
||||
onZoom: onZoom,
|
||||
hideText,
|
||||
};
|
||||
|
||||
return (
|
||||
<TimePickerWithHistory
|
||||
isOnCanvas
|
||||
{...timePickerCommonProps}
|
||||
timeSyncButton={timeSyncButton}
|
||||
isSynced={syncedTimes}
|
||||
widthOverride={splitted ? window.innerWidth / 2 : undefined}
|
||||
onChange={this.onChangeTimePicker}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TimePickerWithHistory
|
||||
isOnCanvas
|
||||
{...timePickerCommonProps}
|
||||
timeSyncButton={timeSyncButton}
|
||||
isSynced={syncedTimes}
|
||||
widthOverride={splitted ? window.innerWidth / 2 : undefined}
|
||||
onChange={onChangeTimePicker}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,15 +18,16 @@ import SearchBarInput from './SearchBarInput';
|
||||
|
||||
describe('SearchBarInput', () => {
|
||||
describe('rendering', () => {
|
||||
const onChange = jest.fn();
|
||||
it('renders as expected with no value', () => {
|
||||
render(<SearchBarInput />);
|
||||
render(<SearchBarInput onChange={onChange} />);
|
||||
const searchBarInput = screen.queryByPlaceholderText('Find...');
|
||||
expect(searchBarInput).toBeInTheDocument();
|
||||
expect(searchBarInput?.getAttribute('value')).toEqual('');
|
||||
});
|
||||
|
||||
it('renders as expected with value', () => {
|
||||
render(<SearchBarInput value="value" />);
|
||||
render(<SearchBarInput onChange={onChange} value="value" />);
|
||||
const searchBarInput = screen.queryByPlaceholderText('Find...');
|
||||
expect(searchBarInput).toBeInTheDocument();
|
||||
expect(searchBarInput?.getAttribute('value')).toEqual('value');
|
||||
|
||||
@@ -12,49 +12,44 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { IconButton, Input } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
value: string | undefined;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export default class SearchBarInput extends React.PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
value: undefined,
|
||||
const SearchBarInput = memo(({ value, onChange }: Props) => {
|
||||
const clearUiFind = () => {
|
||||
onChange('');
|
||||
};
|
||||
|
||||
clearUiFind = () => {
|
||||
this.props.onChange('');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = this.props;
|
||||
|
||||
const suffix = (
|
||||
<>
|
||||
{value && value.length && (
|
||||
<IconButton
|
||||
name="times"
|
||||
onClick={this.clearUiFind}
|
||||
tooltip={t('explore.search-bar-input.suffix.tooltip-clear-input', 'Clear input')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ width: '200px' }}>
|
||||
<Input
|
||||
placeholder={t('explore.search-bar-input.placeholder-find', 'Find...')}
|
||||
onChange={(e) => this.props.onChange(e.currentTarget.value)}
|
||||
suffix={suffix}
|
||||
value={value}
|
||||
const suffix = (
|
||||
<>
|
||||
{value && value.length && (
|
||||
<IconButton
|
||||
name="times"
|
||||
onClick={clearUiFind}
|
||||
tooltip={t('explore.search-bar-input.suffix.tooltip-clear-input', 'Clear input')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ width: '200px' }}>
|
||||
<Input
|
||||
placeholder={t('explore.search-bar-input.placeholder-find', 'Find...')}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
suffix={suffix}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SearchBarInput.displayName = 'SearchBarInput';
|
||||
|
||||
export default SearchBarInput;
|
||||
|
||||
@@ -1,72 +1,63 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, getGrafanaLiveSrv } from '@grafana/runtime';
|
||||
import { Alert, stylesFactory } from '@grafana/ui';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
export interface Props {}
|
||||
|
||||
export interface State {
|
||||
show?: boolean;
|
||||
}
|
||||
export const LiveConnectionWarning = memo(function LiveConnectionWarning() {
|
||||
const [show, setShow] = useState<boolean | undefined>(undefined);
|
||||
const subscriptionRef = useRef<Unsubscribable>();
|
||||
const styles = useStyles2(getStyle);
|
||||
|
||||
export class LiveConnectionWarning extends PureComponent<Props, State> {
|
||||
subscription?: Unsubscribable;
|
||||
styles = getStyle(config.theme2);
|
||||
state: State = {};
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
// Only show the error in development mode
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Wait a second to listen for server errors
|
||||
setTimeout(this.initListener, 1500);
|
||||
const timer = setTimeout(() => {
|
||||
const live = getGrafanaLiveSrv();
|
||||
if (live) {
|
||||
subscriptionRef.current = live.getConnectionState().subscribe({
|
||||
next: (v) => {
|
||||
setShow(!v);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (subscriptionRef.current) {
|
||||
subscriptionRef.current.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
if (show) {
|
||||
if (!contextSrv.isSignedIn || !config.liveEnabled || contextSrv.user.orgRole === OrgRole.None) {
|
||||
return null; // do not show the warning for anonymous users or ones with no org (and /login page etc)
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity={'warning'}
|
||||
className={styles.warn}
|
||||
title={t('live.live-connection-warning.title-connection-to-server-is-lost', 'Connection to server is lost...')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
initListener = () => {
|
||||
const live = getGrafanaLiveSrv();
|
||||
if (live) {
|
||||
this.subscription = live.getConnectionState().subscribe({
|
||||
next: (v) => {
|
||||
this.setState({ show: !v });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show } = this.state;
|
||||
if (show) {
|
||||
if (!contextSrv.isSignedIn || !config.liveEnabled || contextSrv.user.orgRole === OrgRole.None) {
|
||||
return null; // do not show the warning for anonymous users or ones with no org (and /login page etc)
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity={'warning'}
|
||||
className={this.styles.warn}
|
||||
title={t(
|
||||
'live.live-connection-warning.title-connection-to-server-is-lost',
|
||||
'Connection to server is lost...'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const getStyle = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
const getStyle = (theme: GrafanaTheme2) => ({
|
||||
warn: css({
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
@@ -76,4 +67,4 @@ const getStyle = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
zIndex: theme.zIndex.portal,
|
||||
cursor: 'wait',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PureComponent, ChangeEvent } from 'react';
|
||||
import { memo, ChangeEvent } from 'react';
|
||||
|
||||
import {
|
||||
DataTransformerID,
|
||||
@@ -18,13 +18,8 @@ import lightImage from '../images/light/concatenate.svg';
|
||||
|
||||
interface ConcatenateTransformerEditorProps extends TransformerUIProps<ConcatenateTransformerOptions> {}
|
||||
|
||||
export class ConcatenateTransformerEditor extends PureComponent<ConcatenateTransformerEditorProps> {
|
||||
constructor(props: ConcatenateTransformerEditorProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onModeChanged = (value: SelectableValue<ConcatenateFrameNameMode>) => {
|
||||
const { options, onChange } = this.props;
|
||||
export const ConcatenateTransformerEditor = memo(({ options, onChange }: ConcatenateTransformerEditorProps) => {
|
||||
const onModeChanged = (value: SelectableValue<ConcatenateFrameNameMode>) => {
|
||||
const frameNameMode = value.value ?? ConcatenateFrameNameMode.FieldName;
|
||||
onChange({
|
||||
...options,
|
||||
@@ -32,74 +27,63 @@ export class ConcatenateTransformerEditor extends PureComponent<ConcatenateTrans
|
||||
});
|
||||
};
|
||||
|
||||
onLabelChanged = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { options } = this.props;
|
||||
this.props.onChange({
|
||||
const onLabelChanged = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...options,
|
||||
frameNameLabel: evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Render
|
||||
//---------------------------------------------------------
|
||||
const nameModes: Array<SelectableValue<ConcatenateFrameNameMode>> = [
|
||||
{
|
||||
value: ConcatenateFrameNameMode.FieldName,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field',
|
||||
'Copy frame name to field name'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ConcatenateFrameNameMode.Label,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.label-frame',
|
||||
'Add a label with the frame name'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ConcatenateFrameNameMode.Drop,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name',
|
||||
'Ignore the frame name'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
const nameModes: Array<SelectableValue<ConcatenateFrameNameMode>> = [
|
||||
{
|
||||
value: ConcatenateFrameNameMode.FieldName,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field',
|
||||
'Copy frame name to field name'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ConcatenateFrameNameMode.Label,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.label-frame',
|
||||
'Add a label with the frame name'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ConcatenateFrameNameMode.Drop,
|
||||
label: t(
|
||||
'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name',
|
||||
'Ignore the frame name'
|
||||
),
|
||||
},
|
||||
];
|
||||
const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName;
|
||||
|
||||
const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InlineField label={t('transformers.concatenate-transformer-editor.label-name', 'Name')} labelWidth={16} grow>
|
||||
<Select
|
||||
return (
|
||||
<div>
|
||||
<InlineField label={t('transformers.concatenate-transformer-editor.label-name', 'Name')} labelWidth={16} grow>
|
||||
<Select
|
||||
width={36}
|
||||
options={nameModes}
|
||||
value={nameModes.find((v) => v.value === frameNameMode)}
|
||||
onChange={onModeChanged}
|
||||
/>
|
||||
</InlineField>
|
||||
{frameNameMode === ConcatenateFrameNameMode.Label && (
|
||||
<InlineField label={t('transformers.concatenate-transformer-editor.label-label', 'Label')} labelWidth={16} grow>
|
||||
<Input
|
||||
width={36}
|
||||
options={nameModes}
|
||||
value={nameModes.find((v) => v.value === frameNameMode)}
|
||||
onChange={this.onModeChanged}
|
||||
value={options.frameNameLabel ?? ''}
|
||||
placeholder={t('transformers.concatenate-transformer-editor.placeholder-frame', 'Frame')}
|
||||
onChange={onLabelChanged}
|
||||
/>
|
||||
</InlineField>
|
||||
{frameNameMode === ConcatenateFrameNameMode.Label && (
|
||||
<InlineField
|
||||
label={t('transformers.concatenate-transformer-editor.label-label', 'Label')}
|
||||
labelWidth={16}
|
||||
grow
|
||||
>
|
||||
<Input
|
||||
width={36}
|
||||
value={options.frameNameLabel ?? ''}
|
||||
placeholder={t('transformers.concatenate-transformer-editor.placeholder-frame', 'Frame')}
|
||||
onChange={this.onLabelChanged}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ConcatenateTransformerEditor.displayName = 'ConcatenateTransformerEditor';
|
||||
|
||||
export const getConcatenateTransformRegistryItem: () => TransformerRegistryItem<ConcatenateTransformerOptions> =
|
||||
() => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormEvent, PureComponent } from 'react';
|
||||
import { FormEvent, memo } from 'react';
|
||||
|
||||
import { ConstantVariableModel } from '@grafana/data';
|
||||
import { ConstantVariableForm } from 'app/features/dashboard-scene/settings/variables/components/ConstantVariableForm';
|
||||
@@ -7,16 +7,15 @@ import { VariableEditorProps } from '../editor/types';
|
||||
|
||||
export interface Props extends VariableEditorProps<ConstantVariableModel> {}
|
||||
|
||||
export class ConstantVariableEditor extends PureComponent<Props> {
|
||||
onChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
this.props.onPropChange({
|
||||
export const ConstantVariableEditor = memo(({ variable, onPropChange }: Props) => {
|
||||
const onChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
onPropChange({
|
||||
propName: 'query',
|
||||
propValue: event.currentTarget.value,
|
||||
updateOptions: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConstantVariableForm constantValue={this.props.variable.query} onChange={this.onChange} />;
|
||||
}
|
||||
}
|
||||
return <ConstantVariableForm constantValue={variable.query} onChange={onChange} />;
|
||||
});
|
||||
ConstantVariableEditor.displayName = 'ConstantVariableEditor';
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
import { PureComponent } from 'react';
|
||||
import * as React from 'react';
|
||||
import { memo, type KeyboardEvent, type HTMLProps } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { NavigationKey } from '../types';
|
||||
|
||||
export interface Props extends Omit<React.HTMLProps<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
onChange: (value: string) => void;
|
||||
onNavigate: (key: NavigationKey, clearOthers: boolean) => void;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export class VariableInput extends PureComponent<Props> {
|
||||
onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
export const VariableInput = memo(({ value, id, onNavigate, onChange, ...restProps }: Props) => {
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (NavigationKey[event.keyCode] && event.keyCode !== NavigationKey.select) {
|
||||
const clearOthers = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
this.props.onNavigate(event.keyCode, clearOthers);
|
||||
onNavigate(event.keyCode, clearOthers);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange(event.target.value);
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, id, onNavigate, ...restProps } = this.props;
|
||||
return (
|
||||
<input
|
||||
{...restProps}
|
||||
ref={(instance) => {
|
||||
if (instance) {
|
||||
instance.focus();
|
||||
instance.setAttribute('style', `width:${Math.max(instance.width, 150)}px`);
|
||||
}
|
||||
}}
|
||||
id={id}
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
value={value ?? ''}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={t('variable.dropdown.placeholder', 'Enter variable value')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<input
|
||||
{...restProps}
|
||||
ref={(instance) => {
|
||||
if (instance) {
|
||||
instance.focus();
|
||||
instance.setAttribute('style', `width:${Math.max(instance.width, 150)}px`);
|
||||
}
|
||||
}}
|
||||
id={id}
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
value={value ?? ''}
|
||||
onChange={handleChange}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={t('variable.dropdown.placeholder', 'Enter variable value')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
VariableInput.displayName = 'VariableInput';
|
||||
|
||||
+30
-32
@@ -1,4 +1,4 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { ConnectionConfig } from '@grafana/google-sdk';
|
||||
@@ -10,8 +10,8 @@ import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../typ
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<CloudMonitoringOptions, CloudMonitoringSecureJsonData>;
|
||||
|
||||
export class ConfigEditor extends PureComponent<Props> {
|
||||
handleOnOptionsChange = (options: Props['options']) => {
|
||||
export const ConfigEditor = memo(({ options, onOptionsChange }: Props) => {
|
||||
const handleOnOptionsChange = (options: Props['options']) => {
|
||||
if (options.jsonData.privateKeyPath || options.secureJsonFields['privateKey']) {
|
||||
reportInteraction('grafana_cloud_monitoring_config_changed', {
|
||||
authenticationType: 'JWT',
|
||||
@@ -19,34 +19,32 @@ export class ConfigEditor extends PureComponent<Props> {
|
||||
privateKeyPath: !!options.jsonData.privateKeyPath,
|
||||
});
|
||||
}
|
||||
this.props.onOptionsChange(options);
|
||||
onOptionsChange(options);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, onOptionsChange } = this.props;
|
||||
return (
|
||||
<>
|
||||
<DataSourceDescription
|
||||
dataSourceName="Google Cloud Monitoring"
|
||||
docsLink="https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/"
|
||||
hasRequiredFields
|
||||
/>
|
||||
<Divider />
|
||||
<ConnectionConfig {...this.props} onOptionsChange={this.handleOnOptionsChange}></ConnectionConfig>
|
||||
{config.secureSocksDSProxyEnabled && (
|
||||
<>
|
||||
<Divider />
|
||||
<ConfigSection
|
||||
title="Additional settings"
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy."
|
||||
isCollapsible={true}
|
||||
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
|
||||
>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</ConfigSection>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<DataSourceDescription
|
||||
dataSourceName="Google Cloud Monitoring"
|
||||
docsLink="https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/"
|
||||
hasRequiredFields
|
||||
/>
|
||||
<Divider />
|
||||
<ConnectionConfig options={options} onOptionsChange={handleOnOptionsChange}></ConnectionConfig>
|
||||
{config.secureSocksDSProxyEnabled && (
|
||||
<>
|
||||
<Divider />
|
||||
<ConfigSection
|
||||
title="Additional settings"
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy."
|
||||
isCollapsible
|
||||
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
|
||||
>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</ConfigSection>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
ConfigEditor.displayName = 'ConfigEditor';
|
||||
|
||||
+31
-32
@@ -1,4 +1,4 @@
|
||||
import { PureComponent, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
@@ -93,43 +93,42 @@ const CSVWaveEditor = (props: WaveProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export class CSVWavesEditor extends PureComponent<WavesProps> {
|
||||
onChange = (index: number, wave?: CSVWave) => {
|
||||
let waves = [...(this.props.waves ?? defaultCSVWaveQuery)];
|
||||
export const CSVWavesEditor = memo(({ waves, onChange }: WavesProps) => {
|
||||
const handleChange = (index: number, wave?: CSVWave) => {
|
||||
let wavesArray = [...(waves ?? defaultCSVWaveQuery)];
|
||||
if (wave) {
|
||||
waves[index] = { ...wave };
|
||||
wavesArray[index] = { ...wave };
|
||||
} else {
|
||||
// remove the element
|
||||
waves.splice(index, 1);
|
||||
wavesArray.splice(index, 1);
|
||||
}
|
||||
this.props.onChange(waves);
|
||||
onChange(wavesArray);
|
||||
};
|
||||
|
||||
onAdd = () => {
|
||||
const waves = [...(this.props.waves ?? defaultCSVWaveQuery)];
|
||||
waves.push({ ...defaultCSVWaveQuery[0] });
|
||||
this.props.onChange(waves);
|
||||
const onAdd = () => {
|
||||
const wavesArray = [...(waves ?? defaultCSVWaveQuery)];
|
||||
wavesArray.push({ ...defaultCSVWaveQuery[0] });
|
||||
onChange(wavesArray);
|
||||
};
|
||||
|
||||
render() {
|
||||
let waves = this.props.waves ?? defaultCSVWaveQuery;
|
||||
if (!waves.length) {
|
||||
waves = defaultCSVWaveQuery;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{waves.map((wave, index) => (
|
||||
<CSVWaveEditor
|
||||
key={`${index}/${wave.valuesCSV}`}
|
||||
wave={wave}
|
||||
index={index}
|
||||
onAdd={this.onAdd}
|
||||
onChange={this.onChange}
|
||||
last={index === waves.length - 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
let wavesArray = waves ?? defaultCSVWaveQuery;
|
||||
if (!wavesArray.length) {
|
||||
wavesArray = defaultCSVWaveQuery;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{wavesArray.map((wave, index) => (
|
||||
<CSVWaveEditor
|
||||
key={`${index}/${wave.valuesCSV}`}
|
||||
wave={wave}
|
||||
index={index}
|
||||
onAdd={onAdd}
|
||||
onChange={handleChange}
|
||||
last={index === wavesArray.length - 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
CSVWavesEditor.displayName = 'CSVWavesEditor';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
DisplayValueAlignmentFactors,
|
||||
@@ -12,120 +12,131 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { findNumericFieldMinMax } from '@grafana/data/internal';
|
||||
import { BigValueTextMode, BigValueGraphMode } from '@grafana/schema';
|
||||
import { BigValue, DataLinksContextMenu, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui';
|
||||
import { BigValue, DataLinksContextMenu, useTheme2, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui';
|
||||
import { DataLinksContextMenuApi } from '@grafana/ui/internal';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
export class StatPanel extends PureComponent<PanelProps<Options>> {
|
||||
renderComponent = (
|
||||
valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>,
|
||||
menuProps: DataLinksContextMenuApi
|
||||
): JSX.Element => {
|
||||
const { timeRange, options } = this.props;
|
||||
const { value, alignmentFactors, width, height, count } = valueProps;
|
||||
const { openMenu, targetClassName } = menuProps;
|
||||
let sparkline = value.sparkline;
|
||||
if (sparkline) {
|
||||
sparkline.timeRange = timeRange;
|
||||
}
|
||||
export const StatPanel = memo(
|
||||
({
|
||||
timeRange,
|
||||
options,
|
||||
fieldConfig,
|
||||
title,
|
||||
data,
|
||||
replaceVariables,
|
||||
timeZone,
|
||||
height,
|
||||
width,
|
||||
renderCounter,
|
||||
}: PanelProps<Options>) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
return (
|
||||
<BigValue
|
||||
value={value.display}
|
||||
count={count}
|
||||
sparkline={sparkline}
|
||||
colorMode={options.colorMode}
|
||||
graphMode={options.graphMode}
|
||||
justifyMode={options.justifyMode}
|
||||
textMode={this.getTextMode()}
|
||||
alignmentFactors={alignmentFactors}
|
||||
text={options.text}
|
||||
width={width}
|
||||
height={height}
|
||||
theme={config.theme2}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
disableWideLayout={!options.wideLayout}
|
||||
percentChangeColorMode={options.percentChangeColorMode}
|
||||
/>
|
||||
const getTextMode = useCallback(() => {
|
||||
// If we have manually set displayName or panel title switch text mode to value and name
|
||||
if (options.textMode === BigValueTextMode.Auto && (fieldConfig.defaults.displayName || !title)) {
|
||||
return BigValueTextMode.ValueAndName;
|
||||
}
|
||||
|
||||
return options.textMode;
|
||||
}, [options.textMode, fieldConfig.defaults.displayName, title]);
|
||||
|
||||
const renderComponent = useCallback(
|
||||
(
|
||||
valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>,
|
||||
menuProps: DataLinksContextMenuApi
|
||||
): JSX.Element => {
|
||||
const { value, alignmentFactors, width, height, count } = valueProps;
|
||||
const { openMenu, targetClassName } = menuProps;
|
||||
let sparkline = value.sparkline;
|
||||
if (sparkline) {
|
||||
sparkline.timeRange = timeRange;
|
||||
}
|
||||
|
||||
return (
|
||||
<BigValue
|
||||
value={value.display}
|
||||
count={count}
|
||||
sparkline={sparkline}
|
||||
colorMode={options.colorMode}
|
||||
graphMode={options.graphMode}
|
||||
justifyMode={options.justifyMode}
|
||||
textMode={getTextMode()}
|
||||
alignmentFactors={alignmentFactors}
|
||||
text={options.text}
|
||||
width={width}
|
||||
height={height}
|
||||
theme={theme}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
disableWideLayout={!options.wideLayout}
|
||||
percentChangeColorMode={options.percentChangeColorMode}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[theme, timeRange, options, getTextMode]
|
||||
);
|
||||
};
|
||||
|
||||
getTextMode() {
|
||||
const { options, fieldConfig, title } = this.props;
|
||||
const renderValue = useCallback(
|
||||
(valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>): JSX.Element => {
|
||||
const { value } = valueProps;
|
||||
const { getLinks, hasLinks } = value;
|
||||
|
||||
// If we have manually set displayName or panel title switch text mode to value and name
|
||||
if (options.textMode === BigValueTextMode.Auto && (fieldConfig.defaults.displayName || !title)) {
|
||||
return BigValueTextMode.ValueAndName;
|
||||
}
|
||||
if (hasLinks && getLinks) {
|
||||
return (
|
||||
<DataLinksContextMenu links={getLinks}>
|
||||
{(api) => {
|
||||
return renderComponent(valueProps, api);
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return options.textMode;
|
||||
}
|
||||
return renderComponent(valueProps, {});
|
||||
},
|
||||
[renderComponent]
|
||||
);
|
||||
|
||||
renderValue = (valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>): JSX.Element => {
|
||||
const { value } = valueProps;
|
||||
const { getLinks, hasLinks } = value;
|
||||
const getValues = useCallback((): FieldDisplay[] => {
|
||||
let globalRange: NumericRange | undefined = undefined;
|
||||
|
||||
if (hasLinks && getLinks) {
|
||||
return (
|
||||
<DataLinksContextMenu links={getLinks}>
|
||||
{(api) => {
|
||||
return this.renderComponent(valueProps, api);
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderComponent(valueProps, {});
|
||||
};
|
||||
|
||||
getValues = (): FieldDisplay[] => {
|
||||
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||
|
||||
let globalRange: NumericRange | undefined = undefined;
|
||||
|
||||
for (let frame of data.series) {
|
||||
for (let field of frame.fields) {
|
||||
let { config } = field;
|
||||
// mostly copied from fieldOverrides, since they are skipped during streaming
|
||||
// Set the Min/Max value automatically
|
||||
if (field.type === FieldType.number) {
|
||||
if (field.state?.range) {
|
||||
continue;
|
||||
for (let frame of data.series) {
|
||||
for (let field of frame.fields) {
|
||||
let { config } = field;
|
||||
// mostly copied from fieldOverrides, since they are skipped during streaming
|
||||
// Set the Min/Max value automatically
|
||||
if (field.type === FieldType.number) {
|
||||
if (field.state?.range) {
|
||||
continue;
|
||||
}
|
||||
if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) {
|
||||
globalRange = findNumericFieldMinMax(data.series);
|
||||
}
|
||||
const min = config.min ?? globalRange!.min;
|
||||
const max = config.max ?? globalRange!.max;
|
||||
field.state = field.state ?? {};
|
||||
field.state.range = { min, max, delta: max! - min! };
|
||||
}
|
||||
if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) {
|
||||
globalRange = findNumericFieldMinMax(data.series);
|
||||
}
|
||||
const min = config.min ?? globalRange!.min;
|
||||
const max = config.max ?? globalRange!.max;
|
||||
field.state = field.state ?? {};
|
||||
field.state.range = { min, max, delta: max! - min! };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
reduceOptions: options.reduceOptions,
|
||||
replaceVariables,
|
||||
theme: config.theme2,
|
||||
data: data.series,
|
||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||
percentChange: options.showPercentChange,
|
||||
timeZone,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { height, options, width, data, renderCounter } = this.props;
|
||||
return getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
reduceOptions: options.reduceOptions,
|
||||
replaceVariables,
|
||||
theme,
|
||||
data: data.series,
|
||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||
percentChange: options.showPercentChange,
|
||||
timeZone,
|
||||
});
|
||||
}, [data, fieldConfig, theme, options, replaceVariables, timeZone]);
|
||||
|
||||
return (
|
||||
<VizRepeater
|
||||
getValues={this.getValues}
|
||||
getValues={getValues}
|
||||
getAlignmentFactors={getDisplayValueAlignmentFactors}
|
||||
renderValue={this.renderValue}
|
||||
renderValue={renderValue}
|
||||
width={width}
|
||||
height={height}
|
||||
source={data}
|
||||
@@ -136,4 +147,5 @@ export class StatPanel extends PureComponent<PanelProps<Options>> {
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
StatPanel.displayName = 'StatPanel';
|
||||
|
||||
@@ -8782,6 +8782,9 @@
|
||||
"drawer": {
|
||||
"close": "Close"
|
||||
},
|
||||
"error-boundary": {
|
||||
"title": "An unexpected error happened"
|
||||
},
|
||||
"feature-badge": {
|
||||
"experimental": "Experimental",
|
||||
"new": "New!",
|
||||
|
||||
Reference in New Issue
Block a user