diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 4e57dd61656..6cec302e4b4 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -284,4 +284,5 @@ export interface AuthSettings { disableLogin?: boolean; passwordlessEnabled?: boolean; basicAuthStrongPasswordPolicy?: boolean; + disableSignoutMenu?: boolean; } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index a9581366b13..9ed646941d6 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -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 { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ba0f2f436bd..0993e086edb 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -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) { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index d84240556f8..facf246f171 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -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, diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.ts b/public/app/core/components/AppChrome/MegaMenu/utils.ts index 2afb61f8cc4..178d6604dca 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.ts @@ -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'; diff --git a/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx b/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx index 3faa5f24876..937a1fd0f2c 100644 --- a/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx +++ b/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx @@ -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 = () => ( - {config.newsFeedEnabled && ( - <> - + <> + {config.featureToggles.grafanaconThemes && ( + + )} + {config.newsFeedEnabled && ( - - )} + )} + + {!config.auth.disableSignoutMenu && ( + + )} + ); @@ -52,6 +65,7 @@ export function ProfileButton({ profileNode }: Props) { /> {showNewsDrawer && } + {showThemeDrawer && } ); } diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 64d67fa326d..01ba7281c20 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -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; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index dcc1a250133..fcac1bdeefb 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -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 { 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), })); diff --git a/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx b/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx new file mode 100644 index 00000000000..bc3de6ce4d7 --- /dev/null +++ b/public/app/core/components/ThemeSelector/ThemeSelectorDrawer.tsx @@ -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 = ( + + Enjoying the limited edition themes? Tell us what you'd like to see{' '} + + here. + + + ); + + return ( + +
+ {themes.map((themeOption) => ( + onChange(themeOption)} + isSelected={currentTheme.name === themeOption.name} + /> + ))} +
+
+ ); +} + +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 ( +
+
+ +
+ +
+ ); +} + +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; + } +} diff --git a/public/app/core/components/ThemeSelector/getSelectableThemes.ts b/public/app/core/components/ThemeSelector/getSelectableThemes.ts new file mode 100644 index 00000000000..7dc749d9793 --- /dev/null +++ b/public/app/core/components/ThemeSelector/getSelectableThemes.ts @@ -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); +} diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 66417d663af..1de5d88801b 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -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'; diff --git a/public/app/core/services/theme.ts b/public/app/core/services/theme.ts index 6368aa07f76..b244b95e012 100644 --- a/public/app/core/services/theme.ts +++ b/public/app/core/services/theme.ts @@ -50,7 +50,7 @@ export async function changeTheme(themeId: string, runtimeOnly?: boolean) { await service.update({ ...currentPref, - theme: newTheme.colors.mode, + theme: themeId, }); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a2dd8f62e0d..ee429b59821 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {