The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/core/components/SharedPreferences/SharedPreferences.tsx

396 lines
12 KiB

import { css } from '@emotion/css';
import { PureComponent } from 'react';
import * as React from 'react';
import { FeatureState, ThemeRegistryItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { PSEUDO_LOCALE, t, Trans } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
import {
Button,
Field,
FieldSet,
Label,
stylesFactory,
TimeZonePicker,
WeekStartPicker,
FeatureBadge,
Combobox,
ComboboxOption,
TextLink,
WeekStart,
isWeekStart,
} from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { LANGUAGES } from 'app/core/internationalization/constants';
import { LOCALES } from 'app/core/internationalization/locales';
import { PreferencesService } from 'app/core/services/PreferencesService';
import { changeTheme } from 'app/core/services/theme';
import { getSelectableThemes } from '../ThemeSelector/getSelectableThemes';
export interface Props {
resourceUri: string;
AccessControl: FGAC permissions for orgs endpoint on frontend (#41050) * AccessControl: FGAC permissions for orgs endpoint on frontend Protect org update endpoints add or refactor missing right messages cover org page * removing scopes from orgs * Perform permission control with global org * Perform the error handling in case of 403 * Simplify frontend code by requiring read access for sure * Remove roles I added to decrease the number of changes * Remove the check for server admin to reduce the number of changes * change error message * Cleaning todos * Remove unecessary changes * Fix tests * Update test snapshot * Update pkg/api/roles.go Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Format AdminEditOrgPage for linting * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Vardan Torosyan <vardants@gmail.com> * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * Update public/app/features/admin/AdminListOrgsPage.tsx Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * Commit suggestions * Commit suggestion canRead canWrite * fix typo Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Vardan Torosyan <vardants@gmail.com> Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
4 years ago
disabled?: boolean;
preferenceType: 'org' | 'team' | 'user';
onConfirm?: () => Promise<boolean>;
}
export type State = UserPreferencesDTO & {
isLoading: boolean;
isSubmitting: boolean;
};
function getLanguageOptions(): ComboboxOption[] {
const languageOptions = LANGUAGES.map((v) => ({
value: v.code,
label: v.name,
})).sort((a, b) => {
if (a.value === PSEUDO_LOCALE) {
return 1;
}
if (b.value === PSEUDO_LOCALE) {
return -1;
}
return a.label.localeCompare(b.label);
});
const options = [
{
value: '',
label: t('common.locale.default', 'Default'),
},
...languageOptions,
];
return options;
}
function getRegionalFormatOptions(): ComboboxOption[] {
const localeOptions = LOCALES.map((v) => ({
value: v.code,
label: v.name,
})).sort((a, b) => {
return a.label.localeCompare(b.label);
});
const options = [
{
value: '',
label: t('common.locale.default', 'Default'),
},
...localeOptions,
];
return options;
}
export class SharedPreferences extends PureComponent<Props, State> {
service: PreferencesService;
themeOptions: ComboboxOption[];
languageOptions: ComboboxOption[];
regionalFormatOptions: ComboboxOption[];
constructor(props: Props) {
super(props);
this.service = new PreferencesService(props.resourceUri);
this.state = {
isLoading: false,
isSubmitting: false,
theme: '',
timezone: '',
weekStart: '',
language: '',
regionalFormat: '',
queryHistory: { homeTab: '' },
navbar: { bookmarkUrls: [] },
};
const themes = getSelectableThemes();
// Options are translated, so must be called after init but call them
// in constructor to avoid memo-break of array changing every render
this.themeOptions = themes.map((theme) => ({
value: theme.id,
label: getTranslatedThemeName(theme),
group: theme.isExtra ? t('shared-preferences.theme.experimental', 'Experimental') : undefined,
}));
this.languageOptions = getLanguageOptions();
this.regionalFormatOptions = getRegionalFormatOptions();
// Add default option
this.themeOptions.unshift({ value: '', label: t('shared-preferences.theme.default-label', 'Default') });
}
async componentDidMount() {
this.setState({
isLoading: true,
});
const prefs = await this.service.load();
this.setState({
isLoading: false,
homeDashboardUID: prefs.homeDashboardUID,
theme: prefs.theme,
timezone: prefs.timezone,
weekStart: prefs.weekStart,
language: prefs.language,
regionalFormat: prefs.regionalFormat,
queryHistory: prefs.queryHistory,
navbar: prefs.navbar,
});
}
onSubmitForm = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;
if (confirmationResult) {
const { homeDashboardUID, theme, timezone, weekStart, language, regionalFormat, queryHistory, navbar } =
this.state;
reportInteraction('grafana_preferences_save_button_clicked', {
preferenceType: this.props.preferenceType,
theme,
language,
});
this.setState({ isSubmitting: true });
await this.service
.update({
homeDashboardUID,
theme,
timezone,
weekStart,
language,
regionalFormat,
queryHistory,
navbar,
})
.finally(() => {
this.setState({ isSubmitting: false });
});
window.location.reload();
}
};
onThemeChanged = (value: ComboboxOption<string>) => {
this.setState({ theme: value.value });
reportInteraction('grafana_preferences_theme_changed', {
toTheme: value.value,
preferenceType: this.props.preferenceType,
});
if (value.value) {
changeTheme(value.value, true);
}
};
onTimeZoneChanged = (timezone?: string) => {
if (typeof timezone !== 'string') {
DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586) * added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone
6 years ago
return;
}
this.setState({ timezone: timezone });
};
onWeekStartChanged = (weekStart?: WeekStart) => {
this.setState({ weekStart: weekStart ?? '' });
};
onHomeDashboardChanged = (dashboardUID: string) => {
this.setState({ homeDashboardUID: dashboardUID });
};
onLanguageChanged = (language: string) => {
this.setState({ language });
reportInteraction('grafana_preferences_language_changed', {
toLanguage: language,
preferenceType: this.props.preferenceType,
});
};
onLocaleChanged = (regionalFormat: string) => {
this.setState({ regionalFormat });
reportInteraction('grafana_preferences_regional_format_changed', {
toRegionalFormat: regionalFormat,
preferenceType: this.props.preferenceType,
});
};
render() {
const { theme, timezone, weekStart, homeDashboardUID, language, isLoading, isSubmitting, regionalFormat } =
this.state;
AccessControl: FGAC permissions for orgs endpoint on frontend (#41050) * AccessControl: FGAC permissions for orgs endpoint on frontend Protect org update endpoints add or refactor missing right messages cover org page * removing scopes from orgs * Perform permission control with global org * Perform the error handling in case of 403 * Simplify frontend code by requiring read access for sure * Remove roles I added to decrease the number of changes * Remove the check for server admin to reduce the number of changes * change error message * Cleaning todos * Remove unecessary changes * Fix tests * Update test snapshot * Update pkg/api/roles.go Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Format AdminEditOrgPage for linting * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Vardan Torosyan <vardants@gmail.com> * Update public/app/features/admin/AdminEditOrgPage.tsx Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * Update public/app/features/admin/AdminListOrgsPage.tsx Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> * Commit suggestions * Commit suggestion canRead canWrite * fix typo Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Vardan Torosyan <vardants@gmail.com> Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
4 years ago
const { disabled } = this.props;
const styles = getStyles();
const currentThemeOption = this.themeOptions.find((x) => x.value === theme) ?? this.themeOptions[0];
return (
<form onSubmit={this.onSubmitForm} className={styles.form}>
<FieldSet label={<Trans i18nKey="shared-preferences.title">Preferences</Trans>} disabled={disabled}>
<Field
loading={isLoading}
disabled={isLoading}
label={t('shared-preferences.fields.theme-label', 'Interface theme')}
description={
config.featureToggles.grafanaconThemes && config.feedbackLinksEnabled ? (
<Trans i18nKey="shared-preferences.fields.theme-description">
Enjoying the experimental themes? Tell us what you'd like to see{' '}
<TextLink
variant="bodySmall"
external
href="https://docs.google.com/forms/d/e/1FAIpQLSeRKAY8nUMEVIKSYJ99uOO-dimF6Y69_If1Q1jTLOZRWqK1cw/viewform?usp=dialog"
>
here.
</TextLink>
</Trans>
) : undefined
}
>
<Combobox
options={this.themeOptions}
value={currentThemeOption.value}
onChange={this.onThemeChanged}
id="shared-preferences-theme-select"
/>
</Field>
<Field
loading={isLoading}
disabled={isLoading}
label={
<Label htmlFor="home-dashboard-select">
<span className={styles.labelText}>
<Trans i18nKey="shared-preferences.fields.home-dashboard-label">Home Dashboard</Trans>
</span>
</Label>
}
data-testid="User preferences home dashboard drop down"
>
<DashboardPicker
value={homeDashboardUID}
onChange={(v) => this.onHomeDashboardChanged(v?.uid ?? '')}
defaultOptions={true}
isClearable={true}
placeholder={t('shared-preferences.fields.home-dashboard-placeholder', 'Default dashboard')}
inputId="home-dashboard-select"
/>
</Field>
<Field
loading={isLoading}
disabled={isLoading}
label={t('shared-dashboard.fields.timezone-label', 'Timezone')}
data-testid={selectors.components.TimeZonePicker.containerV2}
>
<TimeZonePicker
includeInternal={true}
value={timezone}
onChange={this.onTimeZoneChanged}
inputId="shared-preferences-timezone-picker"
/>
</Field>
<Field
loading={isLoading}
disabled={isLoading}
label={t('shared-preferences.fields.week-start-label', 'Week start')}
data-testid={selectors.components.WeekStartPicker.containerV2}
>
<WeekStartPicker
value={weekStart && isWeekStart(weekStart) ? weekStart : undefined}
onChange={this.onWeekStartChanged}
inputId="shared-preferences-week-start-picker"
/>
</Field>
<Field
loading={isLoading}
disabled={isLoading}
label={
<Label htmlFor="language-preference-select">
<span className={styles.labelText}>
<Trans i18nKey="shared-preferences.fields.language-preference-label">Language</Trans>
</span>
<FeatureBadge featureState={FeatureState.preview} />
</Label>
}
data-testid="User preferences language drop down"
>
<Combobox
value={this.languageOptions.find((lang) => lang.value === language)?.value || ''}
onChange={(lang: ComboboxOption | null) => this.onLanguageChanged(lang?.value ?? '')}
options={this.languageOptions}
placeholder={t('shared-preferences.fields.language-preference-placeholder', 'Choose language')}
id="language-preference-select"
/>
</Field>
{config.featureToggles.localeFormatPreference && (
<Field
loading={isLoading}
disabled={isLoading}
label={
<Label htmlFor="locale-preference">
<span className={styles.labelText}>
<Trans i18nKey="shared-preferences.fields.locale-preference-label">Region format</Trans>
</span>
<FeatureBadge featureState={FeatureState.experimental} />
</Label>
}
description={t(
'shared-preferences.fields.locale-preference-description',
'Choose your region to see the corresponding date, time, and number format'
)}
data-testid="User preferences locale drop down"
>
<Combobox
value={this.regionalFormatOptions.find((loc) => loc.value === regionalFormat)?.value || ''}
onChange={(locale: ComboboxOption | null) => this.onLocaleChanged(locale?.value ?? '')}
options={this.regionalFormatOptions}
placeholder={t('shared-preferences.fields.locale-preference-placeholder', 'Choose region')}
id="locale-preference-select"
/>
</Field>
)}
</FieldSet>
<Button
disabled={isSubmitting}
type="submit"
variant="primary"
data-testid={selectors.components.UserProfile.preferencesSaveButton}
>
<Trans i18nKey="common.save">Save</Trans>
</Button>
</form>
);
}
}
export default SharedPreferences;
const getStyles = stylesFactory(() => {
return {
labelText: css({
marginRight: '6px',
}),
form: css({
width: '100%',
maxWidth: '600px',
}),
};
});
function getTranslatedThemeName(theme: ThemeRegistryItem) {
switch (theme.id) {
case 'dark':
return t('shared.preferences.theme.dark-label', 'Dark');
case 'light':
return t('shared.preferences.theme.light-label', 'Light');
case 'system':
return t('shared.preferences.theme.system-label', 'System preference');
default:
return theme.name;
}
}