diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 35a42b300d..cbabe87e08 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -39,6 +39,7 @@ import { useAfterInitHook } from 'state/useAfterInitHook'; import useSidebarCondensed from 'sidebar/useSidebarCondensed'; import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom'; import { useGetKymaResources } from 'state/kymaResourcesAtom'; +import { Spinner } from 'shared/components/Spinner/Spinner'; export default function App() { const language = useRecoilValue(languageAtom); @@ -59,7 +60,7 @@ export default function App() { useResourceSchemas(); useSidebarCondensed(); - useAuthHandler(); + const { isLoading } = useAuthHandler(); useGetConfiguration(); useGetExtensions(); useGetExtensibilitySchemas(); @@ -75,6 +76,10 @@ export default function App() { useAfterInitHook(kubeconfigIdState); useGetKymaResources(); + if (isLoading) { + return ; + } + return (
diff --git a/src/components/Extensibility/components-form/GenericList.js b/src/components/Extensibility/components-form/GenericList.js index 18341edf0d..d8afcb3342 100644 --- a/src/components/Extensibility/components-form/GenericList.js +++ b/src/components/Extensibility/components-form/GenericList.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { PluginStack, useUIStore } from '@ui-schema/ui-schema'; import { Button } from '@ui5/webcomponents-react'; import { useTranslation } from 'react-i18next'; diff --git a/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss b/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss index fca4f1243b..2e1e9020c2 100644 --- a/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss +++ b/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss @@ -3,6 +3,11 @@ margin-left: 0.25rem !important; margin-right: 0.25rem !important; } + +.banner-carousel { + height: fit-content; +} + .feature-card { position: relative; display: flex; diff --git a/src/components/HelmReleases/HelmReleasesList.js b/src/components/HelmReleases/HelmReleasesList.js index ababeb4705..8ace16c496 100644 --- a/src/components/HelmReleases/HelmReleasesList.js +++ b/src/components/HelmReleases/HelmReleasesList.js @@ -72,36 +72,34 @@ function HelmReleasesList() { ]; return ( - <> - a.releaseName.localeCompare(b.releaseName), - }} - searchSettings={{ - textSearchProperties: [ - 'recentRelease.chart.metadata.name', - 'releaseName', - ], - }} - emptyListProps={{ - subtitleText: ResourceDescription, - url: docsURL, - showButton: false, - }} - readOnly - description={ResourceDescription} - /> - + a.releaseName.localeCompare(b.releaseName), + }} + searchSettings={{ + textSearchProperties: [ + 'recentRelease.chart.metadata.name', + 'releaseName', + ], + }} + emptyListProps={{ + subtitleText: ResourceDescription, + url: docsURL, + showButton: false, + }} + readOnly + description={ResourceDescription} + /> ); } diff --git a/src/components/KymaModules/KymaModulesList.js b/src/components/KymaModules/KymaModulesList.js index 52ed92a242..3f7800fa4e 100644 --- a/src/components/KymaModules/KymaModulesList.js +++ b/src/components/KymaModules/KymaModulesList.js @@ -421,7 +421,7 @@ export default function KymaModulesList({ layoutNumber="StartColumn" windowTitle={t('kyma-modules.title')} headerContent={ - + diff --git a/src/header/NamespaceDropdown/NamespaceDropdown.tsx b/src/header/NamespaceDropdown/NamespaceDropdown.tsx index 0d316f4db4..d62d4d2823 100644 --- a/src/header/NamespaceDropdown/NamespaceDropdown.tsx +++ b/src/header/NamespaceDropdown/NamespaceDropdown.tsx @@ -11,7 +11,7 @@ export function NamespaceDropdown() { let namespaces = []; - if (allNamespaces.length > 0) { + if (allNamespaces && allNamespaces.length > 0) { namespaces.push( + allNamespaces?.map(ns => namespaces.push(), ); diff --git a/src/hooks/useAvailableNamespaces.ts b/src/hooks/useAvailableNamespaces.ts index 288426131f..beddfe01b7 100644 --- a/src/hooks/useAvailableNamespaces.ts +++ b/src/hooks/useAvailableNamespaces.ts @@ -28,7 +28,7 @@ export function useAvailableNamespaces() { useEffect(() => { if (error) { - setNamespaces([]); + setNamespaces(null); return; } const filteredNamespaces = allNamespaces diff --git a/src/resources/createResourceRoutes.js b/src/resources/createResourceRoutes.js index 845145da95..9c1052751c 100644 --- a/src/resources/createResourceRoutes.js +++ b/src/resources/createResourceRoutes.js @@ -84,17 +84,31 @@ const ColumnWrapper = ({ }); const elementListProps = usePrepareListProps({ - ...props, + resourceCustomType: props.resourceCustomType, + resourceType: props.resourceType, + resourceI18Key: props.resourceI18Key, + apiGroup: props.apiGroup, + apiVersion: props.apiVersion, + hasDetailsView: props.hasDetailsView, }); const elementDetailsProps = usePrepareDetailsProps({ - ...props, + resourceCustomType: props.resourceCustomType, + resourceType: props.resourceType, + resourceI18Key: props.resourceI18Key, + apiGroup: props.apiGroup, + apiVersion: props.apiVersion, resourceName: layoutState?.midColumn?.resourceName ?? resourceName, namespaceId: layoutState?.midColumn?.namespaceId ?? namespaceId, + showYamlTab: props.showYamlTab, }); const elementCreateProps = usePrepareCreateProps({ - ...props, + resourceCustomType: props.resourceCustomType, + resourceType: props.resourceType, + resourceTypeForTitle: props.resourceType, + apiGroup: props.apiGroup, + apiVersion: props.apiVersion, }); const listComponent = React.cloneElement(list, { @@ -122,16 +136,13 @@ const ColumnWrapper = ({ detailsMidColumn = detailsComponent; } - const { schema, loading } = useGetSchema({ + const { schema } = useGetSchema({ resource: { group: props?.apiGroup, version: props.apiVersion, kind: props?.resourceType.slice(0, -1), }, }); - if (loading) { - return null; - } const createMidColumn = ( + { +const useGetHeaderHeight = (dynamicPageRef, tabContainerRef) => { const [headerHeight, setHeaderHeight] = useState(undefined); + const [tabContainerHeight, setTabContainerHeight] = useState(undefined); + useEffect(() => { const headerObserver = new ResizeObserver(([header]) => { setHeaderHeight(header.contentRect.height); }); + + const tabContainerObserver = new ResizeObserver(([tabContainer]) => { + setTabContainerHeight(tabContainer.contentRect.height); + }); + if (dynamicPageRef.current) { - // wait for the custom element to be defined (adjust the tag-name if you're using the scoping feature) + // Wait for the custom element to be defined void customElements.whenDefined('ui5-dynamic-page').then(() => { const shadowRoot = dynamicPageRef.current?.shadowRoot; @@ -40,12 +45,13 @@ const useGetHeaderHeight = dynamicPageRef => { return; } - // wait for the shadowRoot to be populated + // Wait for the shadowRoot to be populated const shadowRootObserver = new MutationObserver(() => { const header = shadowRoot.querySelector('header'); + if (header) { - shadowRootObserver.disconnect(); headerObserver.observe(header); + shadowRootObserver.disconnect(); } }); @@ -53,21 +59,24 @@ const useGetHeaderHeight = dynamicPageRef => { const header = shadowRoot.querySelector('header'); if (header) { headerObserver.observe(header); - } else { - return; } } else if (shadowRoot instanceof Node) { shadowRootObserver.observe(shadowRoot, { childList: true }); - } else { - return; } }); } + + if (tabContainerRef.current) { + tabContainerObserver.observe(tabContainerRef.current); + } + return () => { headerObserver.disconnect(); + tabContainerObserver.disconnect(); }; - }, [dynamicPageRef]); - return headerHeight; + }, [dynamicPageRef, tabContainerRef]); + + return { headerHeight, tabContainerHeight }; }; const Column = ({ title, children, columnSpan, image, style = {} }) => { @@ -110,7 +119,11 @@ export const DynamicPageComponent = ({ const [selectedSectionIdState, setSelectedSectionIdState] = useState('view'); const dynamicPageRef = useRef(null); - const headerHeight = useGetHeaderHeight(dynamicPageRef); + const tabContainerRef = useRef(null); + const { headerHeight, tabContainerHeight } = useGetHeaderHeight( + dynamicPageRef, + tabContainerRef, + ); const handleColumnClose = () => { window.history.pushState( @@ -246,44 +259,13 @@ export const DynamicPageComponent = ({ ); - const headerTitle = inlineEditForm ? ( - - - {title} - - {protectedResource && ( - - {protectedResourceWarning} - - )} - {description && ( - - )} - - } - actionsBar={actionsBar} - /> - ) : ( + const headerTitle = ( - <section className={`column-wrapper ${columnWrapperClassName || ''}`}> - {children} - </section> - </ObjectPageHeader> - ) : ( - <DynamicPageHeader className="header-wrapper"> - <section className={`column-wrapper ${columnWrapperClassName || ''}`}> - {children} - </section> - </DynamicPageHeader> - ) + <DynamicPageHeader className="header-wrapper"> + <section className={`column-wrapper ${columnWrapperClassName || ''}`}> + {children} + </section> + </DynamicPageHeader> ) : null; - const [stickyHeaderHeight, setStickyHeaderHeight] = useState(0); + const handlePageRef = dynamicPage => { + if (dynamicPageRef) { + if (typeof dynamicPageRef === 'function') { + dynamicPageRef(dynamicPage); + } else if (dynamicPageRef.current !== undefined) { + dynamicPageRef.current = dynamicPage; + } + } - useEffect(() => { - setTimeout(() => { - setStickyHeaderHeight( - (document.querySelector('.page-header')?.querySelector('header') - ?.clientHeight ?? 0) + - (document - .querySelector('.page-header') - ?.querySelector('ui5-tabcontainer')?.clientHeight ?? 0), - ); - }); - }, []); + const button = dynamicPage?.shadowRoot?.querySelector( + 'ui5-dynamic-page-header-actions', + ); + if (button) { + button.style['display'] = 'none'; + } + }; if (inlineEditForm) { return ( - <ObjectPage + <DynamicPage mode="IconTabBar" className={`page-header ${className}`} headerPinned @@ -352,7 +329,7 @@ export const DynamicPageComponent = ({ titleArea={headerTitle} headerArea={customHeaderContent ?? headerContent} selectedSectionId={selectedSectionIdState} - onBeforeNavigate={e => { + onSelectedSectionChange={e => { if (isFormOpen.formOpen) { e.preventDefault(); } @@ -363,37 +340,40 @@ export const DynamicPageComponent = ({ isFormOpen, setIsFormOpen, () => { - setSelectedSectionIdState(e.detail.sectionId); + setSelectedSectionIdState(e.detail.selectedSectionId); setIsResourceEdited({ isEdited: false, }); }, ); - if (e.detail.sectionId === 'edit') { + if (e.detail.selectedSectionI === 'edit') { setIsFormOpen({ formOpen: true }); } }} + ref={dynamicPage => handlePageRef(dynamicPage)} > - <ObjectPageSection - aria-label="View" - hideTitleText - id="view" - titleText={t('common.tabs.view')} - > - {content} - </ObjectPageSection> - <ObjectPageSection - aria-label="Edit" - hideTitleText - id="edit" - titleText={ - showYamlTab ? t('common.tabs.yaml') : t('common.tabs.edit') - } + <TabContainer + className="tab-container" + style={{ top: `${headerHeight}px` }} + ref={tabContainerRef} + onTabSelect={event => { + const mode = event.detail.tab.getAttribute('data-mode'); + setSelectedSectionIdState(mode); + }} > - {inlineEditForm(stickyHeaderHeight)} - </ObjectPageSection> - </ObjectPage> + <Tab data-mode="view" text={t('common.tabs.view')}></Tab> + <Tab + data-mode="edit" + text={showYamlTab ? t('common.tabs.yaml') : t('common.tabs.edit')} + ></Tab> + </TabContainer> + + {selectedSectionIdState === 'view' && content} + + {selectedSectionIdState === 'edit' && + inlineEditForm(headerHeight + tabContainerHeight)} + </DynamicPage> ); } @@ -404,7 +384,7 @@ export const DynamicPageComponent = ({ titleArea={headerTitle} headerArea={headerContent} footerArea={footer} - ref={dynamicPageRef} + ref={dynamicPage => handlePageRef(dynamicPage)} > {typeof content === 'function' ? content(headerHeight) : content} </DynamicPage> diff --git a/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss b/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss index 1d512b03ed..a1122a9d0b 100644 --- a/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss +++ b/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss @@ -4,6 +4,7 @@ ui5-dynamic-page-title { justify-content: center; min-height: 3rem; + padding-left: 2rem; } .bold-title { @@ -27,10 +28,6 @@ display: none; } - [data-component-name='ObjectPageTitleMiddleSection'] > div { - flex-basis: 100%; - } - &__actions { display: flex; align-items: center; @@ -57,6 +54,17 @@ position: static; padding: unset; } + + .tab-container { + position: sticky; + z-index: 1; + } +} + +ui5-dynamic-page { + .no-shadow { + box-shadow: none; + } } .header-wrapper { @@ -99,10 +107,6 @@ } } -ui5-button[data-component-name='ObjectPageAnchorBarExpandBtn'] { - display: none; -} - @media (max-width: 768px) { .column-wrapper { display: flex; @@ -110,8 +114,3 @@ ui5-button[data-component-name='ObjectPageAnchorBarExpandBtn'] { gap: 12px; } } - -[data-component-name='DynamicPageContent'], -[data-component-name='ObjectPageContent'] { - padding: 0 !important; -} diff --git a/src/shared/components/GenericList/GenericList.js b/src/shared/components/GenericList/GenericList.js index 2c310262fb..e8cf5dab4a 100644 --- a/src/shared/components/GenericList/GenericList.js +++ b/src/shared/components/GenericList/GenericList.js @@ -32,6 +32,7 @@ import pluralize from 'pluralize'; import { isResourceEditedState } from 'state/resourceEditedAtom'; import { isFormOpenState } from 'state/formOpenAtom'; import { handleActionIfFormOpen } from '../UnsavedMessageBox/helpers'; +import './GenericList.scss'; const defaultSort = { name: nameLocaleSort, @@ -113,9 +114,7 @@ export const GenericList = ({ }, [pageSize, pagination]); const { i18n, t } = useTranslation(); - const [currentPage, setCurrentPage] = React.useState( - pagination?.initialPage || 1, - ); + const [currentPage, setCurrentPage] = useState(pagination?.initialPage || 1); const [filteredEntries, setFilteredEntries] = useState(() => sorting(sort, entries), @@ -197,7 +196,7 @@ export const GenericList = ({ const renderTableBody = () => { if (serverDataError) { return ( - <BodyFallback> + <BodyFallback key="tableErrorMessage"> <p>{getErrorMessage(serverDataError)}</p> </BodyFallback> ); @@ -205,7 +204,7 @@ export const GenericList = ({ if (serverDataLoading) { return ( - <BodyFallback> + <BodyFallback key="tableDataLoading"> <Spinner /> </BodyFallback> ); diff --git a/src/shared/components/UI5Panel/UI5Panel.tsx b/src/shared/components/UI5Panel/UI5Panel.tsx index 1aaa63aac5..f2c7dd47f6 100644 --- a/src/shared/components/UI5Panel/UI5Panel.tsx +++ b/src/shared/components/UI5Panel/UI5Panel.tsx @@ -38,7 +38,7 @@ export const UI5Panel = ({ if (headerTop !== '0') setTimeout(() => { const stickyHeader = document - .querySelector('ui5-panel') + .querySelector('.resource-form--panel') ?.shadowRoot?.querySelector('.ui5-panel-root') ?.querySelector( '.ui5-panel-heading-wrapper.ui5-panel-heading-wrapper-sticky', diff --git a/src/state/authDataAtom.ts b/src/state/authDataAtom.ts index c598899f2b..a5dbdfe90d 100644 --- a/src/state/authDataAtom.ts +++ b/src/state/authDataAtom.ts @@ -1,6 +1,6 @@ import { parseOIDCparams } from 'components/Clusters/components/oidc-params'; import { UserManager, User } from 'oidc-client-ts'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { atom, useSetRecoilState, useRecoilValue, RecoilState } from 'recoil'; import { KubeconfigNonOIDCAuth, KubeconfigOIDCAuth } from 'types'; @@ -137,6 +137,7 @@ export function useAuthHandler() { const setAuth = useSetRecoilState(authDataState); const navigate = useNavigate(); const setLastFetched = useSetRecoilState(openapiLastFetchedState); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { console.log( @@ -145,14 +146,17 @@ export function useAuthHandler() { if (!cluster) { setAuth(null); + setIsLoading(false); } else { // don't do the auth flow on cluster list (e.g. after refresh, while the OIDC cluster is still connected) if (window.location.pathname === '/clusters') { + setIsLoading(false); return; } const userCredentials = cluster.currentContext?.user?.user; if (hasNonOidcAuth(userCredentials)) { setAuth(userCredentials as KubeconfigNonOIDCAuth); + setIsLoading(false); } else { const onAfterLogin = () => { if (!getPreviousPath() || getPreviousPath() === '/clusters') { @@ -166,8 +170,12 @@ export function useAuthHandler() { navigate('/cluster/' + encodeURIComponent(cluster.name)); } } + setIsLoading(false); + }; + const onError = () => { + navigate('/clusters'); + setIsLoading(false); }; - const onError = () => navigate('/clusters'); handleLogin({ userCredentials: userCredentials as KubeconfigOIDCAuth, @@ -181,6 +189,8 @@ export function useAuthHandler() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [cluster]); + + return { isLoading }; } export const authDataState: RecoilState<AuthDataState> = atom<AuthDataState>({ diff --git a/src/state/namespacesAtom.ts b/src/state/namespacesAtom.ts index bf94bf69d3..c03fd3b363 100644 --- a/src/state/namespacesAtom.ts +++ b/src/state/namespacesAtom.ts @@ -1,8 +1,8 @@ import { atom, RecoilState } from 'recoil'; -export type NamespacesState = string[]; +export type NamespacesState = string[] | null; -const defaultValue: string[] = []; +const defaultValue = null; export const namespacesState: RecoilState<NamespacesState> = atom< NamespacesState