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:
@@ -284,4 +284,5 @@ export interface AuthSettings {
|
||||
disableLogin?: boolean;
|
||||
passwordlessEnabled?: boolean;
|
||||
basicAuthStrongPasswordPolicy?: boolean;
|
||||
disableSignoutMenu?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function changeTheme(themeId: string, runtimeOnly?: boolean) {
|
||||
|
||||
await service.update({
|
||||
...currentPref,
|
||||
theme: newTheme.colors.mode,
|
||||
theme: themeId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user