ThemeDrawer: Change theme from anywhere and preview them (#100405)

* ThemeDrawer: Change theme from anywhere and preview them

* Update

* added subtitle

* Use new component

* Fixes

* Fix runtime ony wrong prop

* Fixed saving issue

* update lang file

* Fixed circular dep

* fix import
This commit is contained in:
Torkel Ödegaard
2025-03-11 12:03:10 +01:00
committed by GitHub
parent ea89a68028
commit b4366db1f1
13 changed files with 175 additions and 37 deletions
@@ -284,4 +284,5 @@ export interface AuthSettings {
disableLogin?: boolean;
passwordlessEnabled?: boolean;
basicAuthStrongPasswordPolicy?: boolean;
disableSignoutMenu?: boolean;
}
+1
View File
@@ -34,6 +34,7 @@ type FrontendSettingsAuthDTO struct {
DisableLogin bool `json:"disableLogin"`
BasicAuthStrongPasswordPolicy bool `json:"basicAuthStrongPasswordPolicy"`
PasswordlessEnabled bool `json:"passwordlessEnabled"`
DisableSignoutMenu bool `json:"disableSignoutMenu"`
}
type FrontendSettingsBuildInfoDTO struct {
+1
View File
@@ -367,6 +367,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]),
DisableLogin: hs.Cfg.DisableLogin,
BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy,
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
}
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
@@ -296,18 +296,6 @@ func (s *ServiceImpl) getProfileNode(c *contextmodel.ReqContext) *navtree.NavLin
})
}
if !s.cfg.DisableSignoutMenu {
// add sign out first
children = append(children, &navtree.NavLink{
Text: "Sign out",
Id: "sign-out",
Url: s.cfg.AppSubURL + "/logout",
Icon: "arrow-from-right",
Target: "_self",
HideFromTabs: true,
})
}
return &navtree.NavLink{
Text: c.SignedInUser.GetName(),
SubTitle: login,
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { NavModelItem } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
@@ -9,7 +10,6 @@ import { ShowModalReactEvent } from '../../../../types/events';
import appEvents from '../../../app_events';
import { getFooterLinks } from '../../Footer/Footer';
import { HelpModal } from '../../help/HelpModal';
import { MEGA_MENU_TOGGLE_ID } from '../TopBar/SingleTopBar';
import { DOCK_MENU_BUTTON_ID, MEGA_MENU_HEADER_TOGGLE_ID } from './MegaMenuHeader';
@@ -8,6 +8,7 @@ import { Dropdown, Menu, MenuItem, ToolbarButton, useStyles2 } from '@grafana/ui
import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization';
import { ThemeSelectorDrawer } from '../../ThemeSelector/ThemeSelectorDrawer';
import { enrichWithInteractionTracking } from '../MegaMenu/utils';
import { NewsContainer } from '../News/NewsDrawer';
@@ -21,6 +22,7 @@ export function ProfileButton({ profileNode }: Props) {
const styles = useStyles2(getStyles);
const node = enrichWithInteractionTracking(cloneDeep(profileNode), false);
const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false);
const [showThemeDrawer, onToggleThemeDrawer] = useToggle(false);
if (!node) {
return null;
@@ -28,16 +30,27 @@ export function ProfileButton({ profileNode }: Props) {
const renderMenu = () => (
<TopNavBarMenu node={profileNode}>
{config.newsFeedEnabled && (
<>
<Menu.Divider />
<>
{config.featureToggles.grafanaconThemes && (
<MenuItem icon="palette" onClick={onToggleThemeDrawer} label={t('profile.change-theme', 'Change theme')} />
)}
{config.newsFeedEnabled && (
<MenuItem
icon="rss"
onClick={onToggleShowNewsDrawer}
label={t('navigation.rss-button', 'Latest from the blog')}
/>
</>
)}
)}
<Menu.Divider />
{!config.auth.disableSignoutMenu && (
<MenuItem
url={`${config.appSubUrl}/logout`}
label={t('nav.sign-out.title', 'Sign out')}
icon="arrow-from-right"
target={'_self'}
/>
)}
</>
</TopNavBarMenu>
);
@@ -52,6 +65,7 @@ export function ProfileButton({ profileNode }: Props) {
/>
</Dropdown>
{showNewsDrawer && <NewsContainer onClose={onToggleShowNewsDrawer} />}
{showThemeDrawer && <ThemeSelectorDrawer onClose={onToggleThemeDrawer} />}
</>
);
}
@@ -5,6 +5,7 @@ import { memo } from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Dropdown, Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization';
@@ -25,8 +26,6 @@ import { SignInLink } from './SignInLink';
import { TopNavBarMenu } from './TopNavBarMenu';
import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
export const MEGA_MENU_TOGGLE_ID = 'mega-menu-toggle';
interface Props {
sectionNav: NavModelItem;
pageNav?: NavModelItem;
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { PureComponent } from 'react';
import * as React from 'react';
import { FeatureState, getBuiltInThemes, ThemeRegistryItem } from '@grafana/data';
import { FeatureState, ThemeRegistryItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime';
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
@@ -26,6 +26,9 @@ import { t, Trans } from 'app/core/internationalization';
import { LANGUAGES, PSEUDO_LOCALE } from 'app/core/internationalization/constants';
import { PreferencesService } from 'app/core/services/PreferencesService';
import { changeTheme } from 'app/core/services/theme';
import { getSelectableThemes } from '../ThemeSelector/getSelectableThemes';
export interface Props {
resourceUri: string;
disabled?: boolean;
@@ -82,21 +85,9 @@ export class SharedPreferences extends PureComponent<Props, State> {
navbar: { bookmarkUrls: [] },
};
const allowedExtraThemes = [];
const themes = getSelectableThemes();
if (config.featureToggles.extraThemes) {
allowedExtraThemes.push('debug');
}
if (config.featureToggles.grafanaconThemes) {
allowedExtraThemes.push('desertbloom');
allowedExtraThemes.push('gildedgrove');
allowedExtraThemes.push('sapphiredusk');
allowedExtraThemes.push('tron');
allowedExtraThemes.push('gloom');
}
this.themeOptions = getBuiltInThemes(allowedExtraThemes).map((theme) => ({
this.themeOptions = themes.map((theme) => ({
value: theme.id,
label: getTranslatedThemeName(theme),
}));
@@ -0,0 +1,121 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, ThemeRegistryItem } from '@grafana/data';
import { Drawer, RadioButtonDot, TextLink, useStyles2, useTheme2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { changeTheme } from 'app/core/services/theme';
import { ThemePreview } from '../Theme/ThemePreview';
import { getSelectableThemes } from './getSelectableThemes';
interface Props {
onClose: () => void;
}
export function ThemeSelectorDrawer({ onClose }: Props) {
const styles = useStyles2(getStyles);
const themes = getSelectableThemes();
const currentTheme = useTheme2();
const onChange = (theme: ThemeRegistryItem) => {
changeTheme(theme.id, false);
};
const subTitle = (
<Trans i18nKey="shared-preferences.fields.theme-description">
Enjoying the limited edition 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>
);
return (
<Drawer title={t('profile.change-theme', 'Change theme')} onClose={onClose} size="md" subtitle={subTitle}>
<div className={styles.grid} role="radiogroup">
{themes.map((themeOption) => (
<ThemeCard
themeOption={themeOption}
key={themeOption.id}
onSelect={() => onChange(themeOption)}
isSelected={currentTheme.name === themeOption.name}
/>
))}
</div>
</Drawer>
);
}
interface ThemeCardProps {
themeOption: ThemeRegistryItem;
isSelected?: boolean;
onSelect: () => void;
}
function ThemeCard({ themeOption, isSelected, onSelect }: ThemeCardProps) {
const theme = themeOption.build();
const label = getTranslatedThemeName(themeOption);
const styles = useStyles2(getStyles);
return (
<div className={styles.card} onClick={onSelect}>
<div className={styles.header}>
<RadioButtonDot
id={`theme-${theme.name}`}
name={'theme'}
label={label}
onChange={onSelect}
checked={isSelected}
/>
</div>
<ThemePreview theme={theme} />
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
grid: css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gridAutoRows: `250px`,
gap: theme.spacing(2),
}),
card: css({
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
display: 'flex',
flexDirection: 'column',
cursor: 'pointer',
'&:hover': {
border: `1px solid ${theme.colors.border.medium}`,
},
}),
header: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
padding: theme.spacing(1),
// The RadioButtonDot is not correctly implemented at the moment, missing cursor (And click ability for the label and input)
'> label': {
cursor: 'pointer',
},
}),
};
};
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;
}
}
@@ -0,0 +1,20 @@
import { getBuiltInThemes } from '@grafana/data';
import { config } from '@grafana/runtime';
export function getSelectableThemes() {
const allowedExtraThemes = [];
if (config.featureToggles.extraThemes) {
allowedExtraThemes.push('debug');
}
if (config.featureToggles.grafanaconThemes) {
allowedExtraThemes.push('desertbloom');
allowedExtraThemes.push('gildedgrove');
allowedExtraThemes.push('sapphiredusk');
allowedExtraThemes.push('tron');
allowedExtraThemes.push('gloom');
}
return getBuiltInThemes(allowedExtraThemes);
}
+1
View File
@@ -17,3 +17,4 @@ export const EDIT_PANEL_ID = 23763571993;
export const DEFAULT_PER_PAGE_PAGINATION = 40;
export const LS_VISUALIZATION_SELECT_TAB_KEY = 'VisualizationSelectPane.ListMode';
export const MEGA_MENU_TOGGLE_ID = 'mega-menu-toggle';
+1 -1
View File
@@ -50,7 +50,7 @@ export async function changeTheme(themeId: string, runtimeOnly?: boolean) {
await service.update({
...currentPref,
theme: newTheme.colors.mode,
theme: themeId,
});
}
+2 -1
View File
@@ -3123,7 +3123,8 @@
"old-password-required": "Old password is required",
"passwords-must-match": "Passwords must match",
"strong-password-validation-register": "Password does not comply with the strong password policy"
}
},
"change-theme": "Change theme"
},
"public-dashboard": {
"acknowledgment-checkboxes": {