mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-02 04:13:17 +00:00
feat(ui): store user preferences to localStorage (#21128)
* feat(ui): store user preferences to localStorage * fix domain spec * fix sidebar causing unwanted failures * fix playwright adjust expandable cards * fix empty condition for cards * update condition (cherry picked from commit 3490a06ca2569bbf01f8c65948a21b7a0e59c0c8)
This commit is contained in:
parent
bb8673c0a7
commit
064d91b7eb
@ -513,6 +513,10 @@ test.describe('Domains', () => {
|
||||
await domain.create(apiContext);
|
||||
await page.reload();
|
||||
await sidebarClick(page, SidebarItem.DOMAIN);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector(`[data-testid="loader"]`, {
|
||||
state: 'hidden',
|
||||
});
|
||||
await selectDomain(page, domain.data);
|
||||
|
||||
await addTagsAndGlossaryToDomain(page, {
|
||||
@ -522,6 +526,10 @@ test.describe('Domains', () => {
|
||||
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.DOMAIN);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector(`[data-testid="loader"]`, {
|
||||
state: 'hidden',
|
||||
});
|
||||
await selectDomain(page, domain.data);
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
@ -625,7 +625,13 @@ export const addTagsAndGlossaryToDomain = async (
|
||||
const input = page.locator(`${container} #tagsForm_tags`);
|
||||
await input.click();
|
||||
await input.fill(value);
|
||||
await page.getByTestId(`tag-${value}`).click();
|
||||
const tag = page.getByTestId(`tag-${value}`);
|
||||
if (containerType === 'glossary') {
|
||||
// To avoid clicking on white space between checkbox and text
|
||||
await tag.locator('.ant-select-tree-checkbox').click();
|
||||
} else {
|
||||
await tag.click();
|
||||
}
|
||||
|
||||
// Save and wait for response
|
||||
const updateResponse = page.waitForResponse(
|
||||
|
@ -45,5 +45,9 @@ export const loginAsAdmin = async (page: Page, admin: AdminClass) => {
|
||||
await admin.logout(page);
|
||||
await page.waitForURL('**/signin');
|
||||
await admin.login(page);
|
||||
|
||||
// Close the leftside bar to run tests smoothly
|
||||
await page.getByTestId('sidebar-toggle').click();
|
||||
|
||||
await page.waitForURL('**/my-data');
|
||||
};
|
||||
|
@ -13,7 +13,7 @@
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore';
|
||||
import { LineageSettings } from '../../generated/configuration/lineageSettings';
|
||||
import { SettingType } from '../../generated/settings/settings';
|
||||
@ -36,7 +36,6 @@ const AppContainer = () => {
|
||||
const AuthenticatedRouter = applicationRoutesClass.getRouteElements();
|
||||
const ApplicationExtras = applicationsClassBase.getApplicationExtension();
|
||||
const { isAuthenticated } = useApplicationStore();
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(true);
|
||||
|
||||
const { setConfig, bannerDetails } = useLimitStore();
|
||||
|
||||
@ -71,16 +70,13 @@ const AppContainer = () => {
|
||||
['extra-banner']: Boolean(bannerDetails),
|
||||
})}>
|
||||
{/* Render left side navigation */}
|
||||
<LeftSidebar isSidebarCollapsed={isSidebarCollapsed} />
|
||||
<LeftSidebar />
|
||||
|
||||
{/* Render main content */}
|
||||
<Layout>
|
||||
{/* Render Appbar */}
|
||||
{isProtectedRoute(location.pathname) && isAuthenticated ? (
|
||||
<NavBar
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
toggleSideBar={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
||||
/>
|
||||
<NavBar />
|
||||
) : null}
|
||||
|
||||
{/* Render main content */}
|
||||
|
@ -182,7 +182,8 @@ export const DomainLabelV2 = <
|
||||
{selectableList}
|
||||
</div>
|
||||
),
|
||||
}}>
|
||||
}}
|
||||
isExpandDisabled={!Array.isArray(domainLink)}>
|
||||
<div className="d-flex items-center gap-1 flex-wrap">
|
||||
{domainLink}
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import { Typography } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
|
||||
import { TabSpecificField } from '../../../enums/entity.enum';
|
||||
@ -76,7 +77,8 @@ export const OwnerLabelV2 = <
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId={dataTestId}>
|
||||
dataTestId={dataTestId}
|
||||
isExpandDisabled={isEmpty(data.owners)}>
|
||||
{getOwnerVersionLabel(
|
||||
data,
|
||||
isVersionView ?? false,
|
||||
|
@ -100,7 +100,8 @@ export const ReviewerLabelV2 = <
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId="glossary-reviewer">
|
||||
dataTestId="glossary-reviewer"
|
||||
isExpandDisabled={!hasReviewers}>
|
||||
<div data-testid="glossary-reviewer-name">
|
||||
{getOwnerVersionLabel(
|
||||
data,
|
||||
|
@ -13,7 +13,7 @@
|
||||
import { Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import { cloneDeep, includes, isEqual } from 'lodash';
|
||||
import { cloneDeep, includes, isEmpty, isEqual } from 'lodash';
|
||||
import { default as React, useMemo } from 'react';
|
||||
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
|
||||
import { TabSpecificField } from '../../../enums/entity.enum';
|
||||
@ -100,7 +100,7 @@ export const DomainExpertWidget = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{editOwnerPermission && domain.experts && domain.experts.length === 0 && (
|
||||
{editOwnerPermission && domain.experts?.length === 0 && (
|
||||
<UserSelectableList
|
||||
hasPermission={editOwnerPermission}
|
||||
popoverProps={{ placement: 'topLeft' }}
|
||||
@ -123,7 +123,8 @@ export const DomainExpertWidget = () => {
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId="domain-expert-name">
|
||||
dataTestId="domain-expert-name"
|
||||
isExpandDisabled={isEmpty(domain.experts)}>
|
||||
{content}
|
||||
</ExpandableCard>
|
||||
);
|
||||
|
@ -147,7 +147,8 @@ const GlossaryTermReferences = () => {
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId="references-container">
|
||||
dataTestId="references-container"
|
||||
isExpandDisabled={isEmpty(references)}>
|
||||
{isVersionView ? (
|
||||
getVersionReferenceElements()
|
||||
) : (
|
||||
|
@ -193,7 +193,8 @@ const GlossaryTermSynonyms = () => {
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId="synonyms-container">
|
||||
dataTestId="synonyms-container"
|
||||
isExpandDisabled={isEmpty(synonyms)}>
|
||||
{isViewMode ? (
|
||||
getSynonymsContainer()
|
||||
) : (
|
||||
|
@ -250,7 +250,8 @@ const RelatedTerms = () => {
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId="related-term-container">
|
||||
dataTestId="related-term-container"
|
||||
isExpandDisabled={selectedOption.length === 0}>
|
||||
{isIconVisible ? (
|
||||
relatedTermsContainer
|
||||
) : (
|
||||
|
@ -219,7 +219,8 @@ const RelatedMetrics: FC<RelatedMetricsProps> = ({
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}>
|
||||
}}
|
||||
isExpandDisabled={isEmpty(relatedMetrics)}>
|
||||
<Row gutter={[0, 8]}>{content}</Row>
|
||||
</ExpandableCard>
|
||||
);
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
SIDEBAR_NESTED_KEYS,
|
||||
} from '../../../constants/LeftSidebar.constants';
|
||||
import { SidebarItem } from '../../../enums/sidebar.enum';
|
||||
import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation';
|
||||
import { useCustomPages } from '../../../hooks/useCustomPages';
|
||||
@ -32,18 +33,16 @@ import BrandImage from '../../common/BrandImage/BrandImage';
|
||||
import './left-sidebar.less';
|
||||
import { LeftSidebarItem as LeftSidebarItemType } from './LeftSidebar.interface';
|
||||
import LeftSidebarItem from './LeftSidebarItem.component';
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
const LeftSidebar = ({
|
||||
isSidebarCollapsed,
|
||||
}: {
|
||||
isSidebarCollapsed: boolean;
|
||||
}) => {
|
||||
const LeftSidebar = () => {
|
||||
const location = useCustomLocation();
|
||||
const { t } = useTranslation();
|
||||
const { onLogoutHandler } = useApplicationStore();
|
||||
const [showConfirmLogoutModal, setShowConfirmLogoutModal] = useState(false);
|
||||
const {
|
||||
preferences: { isSidebarCollapsed },
|
||||
} = useCurrentUserPreferences();
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const isDirectionRTL = useMemo(() => i18n.dir() === 'rtl', [i18n]);
|
||||
|
@ -19,7 +19,7 @@ describe('LeftSidebar', () => {
|
||||
it('renders sidebar links correctly', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<LeftSidebar isSidebarCollapsed={false} />
|
||||
<LeftSidebar />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
|
@ -57,6 +57,7 @@ import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocket
|
||||
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
||||
import { EntityReference } from '../../generated/entity/type';
|
||||
import { BackgroundJob, JobType } from '../../generated/jobs/backgroundJob';
|
||||
import { useCurrentUserPreferences } from '../../hooks/currentUserStore/useCurrentUserStore';
|
||||
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
|
||||
import { useDomainStore } from '../../hooks/useDomainStore';
|
||||
import { getVersion } from '../../rest/miscAPI';
|
||||
@ -96,13 +97,7 @@ import popupAlertsCardsClassBase from './PopupAlertClassBase';
|
||||
|
||||
const cookieStorage = new CookieStorage();
|
||||
|
||||
const NavBar = ({
|
||||
isSidebarCollapsed = true,
|
||||
toggleSideBar,
|
||||
}: {
|
||||
isSidebarCollapsed?: boolean;
|
||||
toggleSideBar?: () => void;
|
||||
}) => {
|
||||
const NavBar = () => {
|
||||
const { isTourOpen: isTourRoute } = useTourProvider();
|
||||
const { onUpdateCSVExportJob } = useEntityExportModalProvider();
|
||||
const { handleDeleteEntityWebsocketResponse } = useAsyncDeleteProvider();
|
||||
@ -123,6 +118,10 @@ const NavBar = ({
|
||||
const [isFeatureModalOpen, setIsFeatureModalOpen] = useState<boolean>(false);
|
||||
const [version, setVersion] = useState<string>();
|
||||
const [isDomainDropdownOpen, setIsDomainDropdownOpen] = useState(false);
|
||||
const {
|
||||
preferences: { isSidebarCollapsed },
|
||||
setPreference,
|
||||
} = useCurrentUserPreferences();
|
||||
|
||||
const fetchOMVersion = async () => {
|
||||
try {
|
||||
@ -423,7 +422,9 @@ const NavBar = ({
|
||||
}
|
||||
size="middle"
|
||||
type="text"
|
||||
onClick={toggleSideBar}
|
||||
onClick={() =>
|
||||
setPreference({ isSidebarCollapsed: !isSidebarCollapsed })
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<GlobalSearchBar />
|
||||
|
@ -427,7 +427,8 @@ const TagsContainerV2 = ({
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}
|
||||
dataTestId={isGlossaryType ? 'glossary-container' : 'tags-container'}>
|
||||
dataTestId={isGlossaryType ? 'glossary-container' : 'tags-container'}
|
||||
isExpandDisabled={isEmpty(tags?.[tagType])}>
|
||||
{suggestionDataRender ?? (
|
||||
<>
|
||||
{tagBody}
|
||||
|
@ -273,7 +273,7 @@ h2.rotated-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-card-head {
|
||||
& > .ant-card-head {
|
||||
padding: 0px 16px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
import { useApplicationStore } from '../useApplicationStore';
|
||||
|
||||
interface UserPreferences {
|
||||
isSidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
interface Store {
|
||||
preferences: Record<string, UserPreferences>;
|
||||
setUserPreference: (
|
||||
userName: string,
|
||||
preferences: Partial<UserPreferences>
|
||||
) => void;
|
||||
getUserPreference: (userName: string) => UserPreferences;
|
||||
clearUserPreference: (userName: string) => void;
|
||||
}
|
||||
|
||||
const defaultPreferences: UserPreferences = {
|
||||
isSidebarCollapsed: false,
|
||||
// Add default values for other preferences
|
||||
};
|
||||
|
||||
export const usePersistentStorage = create<Store>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
preferences: {},
|
||||
|
||||
setUserPreference: (
|
||||
userName: string,
|
||||
newPreferences: Partial<UserPreferences>
|
||||
) => {
|
||||
set((state) => ({
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
[userName]: {
|
||||
...defaultPreferences,
|
||||
...state.preferences[userName],
|
||||
...newPreferences,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
getUserPreference: (userName: string) => {
|
||||
const state = get();
|
||||
|
||||
return state.preferences[userName] || defaultPreferences;
|
||||
},
|
||||
|
||||
clearUserPreference: (userName: string) => {
|
||||
set((state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [userName]: _, ...rest } = state.preferences;
|
||||
|
||||
return { preferences: rest };
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'user-preferences-store',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Hook to easily access current user's preferences
|
||||
export const useCurrentUserPreferences = () => {
|
||||
const currentUser = useApplicationStore((state) => state.currentUser);
|
||||
const { preferences, setUserPreference } = usePersistentStorage();
|
||||
|
||||
if (!currentUser?.name) {
|
||||
return {
|
||||
preferences: defaultPreferences,
|
||||
setPreference: () => {
|
||||
// update the user name in the local storage
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
preferences: preferences[currentUser.name] || defaultPreferences,
|
||||
setPreference: (newPreferences: Partial<UserPreferences>) =>
|
||||
setUserPreference(currentUser.name, newPreferences),
|
||||
};
|
||||
};
|
@ -69,7 +69,8 @@ export const FrequentlyJoinedTables = () => {
|
||||
cardProps={{
|
||||
title: t('label.frequently-joined-table-plural'),
|
||||
}}
|
||||
dataTestId="frequently-joint-data-container">
|
||||
dataTestId="frequently-joint-data-container"
|
||||
isExpandDisabled={isEmpty(joinedTables)}>
|
||||
{content}
|
||||
</ExpandableCard>
|
||||
);
|
||||
|
@ -84,7 +84,8 @@ export const PartitionedKeys = () => {
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
title: t('label.table-partition-plural'),
|
||||
}}>
|
||||
}}
|
||||
isExpandDisabled={isEmpty(partitionColumnDetails)}>
|
||||
{content}
|
||||
</ExpandableCard>
|
||||
);
|
||||
|
@ -178,7 +178,8 @@ const TableConstraints = () => {
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
title: header,
|
||||
}}>
|
||||
}}
|
||||
isExpandDisabled={isEmpty(data?.tableConstraints)}>
|
||||
{content}
|
||||
</ExpandableCard>
|
||||
);
|
||||
|
@ -77,7 +77,6 @@
|
||||
.ant-card.new-header-border-card {
|
||||
height: auto !important;
|
||||
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
.ant-card-head {
|
||||
background: @grey-50;
|
||||
border-radius: @border-radius-sm;
|
||||
@ -89,6 +88,8 @@
|
||||
.ant-card-body {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
transition: all 200ms ease;
|
||||
transition-property: height, left, top;
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
@ -120,5 +121,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-extra {
|
||||
margin-left: @size-sm;
|
||||
}
|
||||
|
||||
border: 1px solid @grey-15;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user