diff --git a/src/app.scss b/src/app.scss index 203a04c9..ca471551 100644 --- a/src/app.scss +++ b/src/app.scss @@ -44,10 +44,19 @@ block-size: 100vh; } -.pf-v5-c-page__main-breadcrumb { +.files-overview-header { grid-area: topbar; + + /* Chrome specific alignment issue */ + align-items: center; + + /* Override different background color from the */ + .pf-v5-c-page__main-breadcrumb { + background-color: unset + } } + .pf-v5-c-sidebar__content > .pf-v5-c-card { grid-area: content; } @@ -74,37 +83,64 @@ @extend .ct-card; } -// We have to override PatternFly assumptions, as PatternFly does not expect -// widgets other than breadcrumbs within a breadcrumbs bar. We should probably -// reparent these and separate the breadcrumb into its own widget on the top -// bar instead. -.pf-v5-c-page__main-breadcrumb { - .pf-v5-c-button:not(.breadcrumb-button):has(.pf-v5-svg:last-child) { - // PatternFly doesn't expect icons in buttons in breadcrumbs, so we need to adjust for that - >.pf-v5-c-button__icon { - margin-inline: 0; +.files-overview-header { + gap: var(--pf-v5-global--spacer--sm); + display: flex; + /* Align page breadcrumb centered */ + align-items: baseline; + + /* Drop PF padding */ + .pf-v5-c-page__main-breadcrumb { + padding: 0; + display: inline-block; + } + + .pf-v5-c-breadcrumb { + margin-block: 0; + margin-inline: var(--pf-v5-global--spacer--sm); + } +} + +.pf-v5-c-breadcrumb { + margin-block: 0; + margin-inline: var(--pf-v5-global--spacer--md); +} + +.pf-v5-c-breadcrumb__list { + // Make sure all breadcrumb text is aligned properly, even if different heights (including icon) + align-items: baseline; + + // Style the breadcrumb component as a path + .pf-v5-c-breadcrumb__item-divider { + > svg { + display: none; + } + + &::after { + content: "/"; } } - .pf-v5-c-menu-toggle { - padding-inline: var(--pf-v5-global--spacer--md) calc(var(--pf-v5-global--spacer--md) * 0.75); + .pf-v5-c-breadcrumb__item { + // Use the default font size, not the smaller size + font-size: var(--pf-v5-global--FontSize--md); + } + + // Size, align, and space icon correctly + .breadcrumb-hdd-icon { + // Set the size to a large icon + block-size: var(--pf-v5-global--icon--FontSize--lg); + // Width should resolve itself based on height and aspect ratio + inline-size: auto; + // Align to the middle (as one would expect) + vertical-align: middle; + // Fix the offset problem so it's properly aligned to middle + margin-block-start: calc(1ex - 1cap); } } .breadcrumb { &-button { - padding-inline: 0; - // HACK: override PF button behaviour, breadcrumbs should - // be links not button and then we should be able to remove these - // overrides. - // https://github.com/cockpit-project/cockpit-files/issues/641 - overflow-wrap: break-word; - white-space: normal; - - &.breadcrumb-0 { - margin-inline-start: var(--pf-v5-global--spacer--sm); - } - &-edit-apply, &-edit-cancel { padding-inline: var(--pf-v5-global--spacer--sm); @@ -130,10 +166,6 @@ } } -.path-divider { - color: var(--pf-v5-global--Color--200); -} - .view-toggle-group { .pf-c-menu-toggle__button { display: flex; @@ -198,3 +230,15 @@ padding-block-start: 0; } } + +// // FIXME: Promote the CSS below to overrides, open PF issues // // + +// PatternFly always adds a margin after images inside of widgets with pf-m-end, which is incorrect when it's the last element +.pf-v5-c-button__icon.pf-m-start:last-child { + margin-inline-end: 0; +} + +// PF menu toggles are no longer spaced consistently +.pf-v5-c-menu-toggle { + padding-inline: var(--pf-v5-global--spacer--md) calc(var(--pf-v5-global--spacer--md) * 0.75); +} diff --git a/src/files-breadcrumbs.tsx b/src/files-breadcrumbs.tsx index c7710fa0..0f6fca3a 100644 --- a/src/files-breadcrumbs.tsx +++ b/src/files-breadcrumbs.tsx @@ -19,15 +19,15 @@ import React from "react"; import { AlertVariant } from "@patternfly/react-core/dist/esm/components/Alert"; +import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb"; import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Divider } from "@patternfly/react-core/dist/esm/components/Divider"; import { Dropdown, DropdownItem, DropdownList } from "@patternfly/react-core/dist/esm/components/Dropdown"; import { MenuToggle, MenuToggleElement } from "@patternfly/react-core/dist/esm/components/MenuToggle"; -import { PageBreadcrumb } from "@patternfly/react-core/dist/esm/components/Page"; +import { PageBreadcrumb, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput"; import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip"; -import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex"; -import { CheckIcon, HddIcon, PencilAltIcon, StarIcon, TimesIcon } from "@patternfly/react-icons"; +import { CheckIcon, OutlinedHddIcon, PencilAltIcon, StarIcon, TimesIcon } from "@patternfly/react-icons"; import { useInit } from "hooks.js"; import cockpit from "cockpit"; @@ -37,29 +37,6 @@ import { basename } from "./common"; const _ = cockpit.gettext; -function useHostname() { - const [hostname, setHostname] = React.useState(null); - - React.useEffect(() => { - const client = cockpit.dbus('org.freedesktop.hostname1'); - const hostname1 = client.proxy('org.freedesktop.hostname1', '/org/freedesktop/hostname1'); - - function changed() { - if (hostname1.valid && typeof hostname1.Hostname === 'string') { - setHostname(hostname1.Hostname); - } - } - - hostname1.addEventListener("changed", changed); - return () => { - hostname1.removeEventListener("changed", changed); - client.close(); - }; - }, []); - - return hostname; -} - function BookmarkButton({ path }: { path: string[] }) { const [isOpen, setIsOpen] = React.useState(false); const [user, setUser] = React.useState(null); @@ -205,15 +182,71 @@ function BookmarkButton({ path }: { path: string[] }) { ); } +const PathBreadcrumbs = ({ path }: { path: string[] }) => { + // HACK: strip extraneous slashes as PF's breadcrumb can't handle them + // Refactor the path to be the fullpath not an array of strings + if (path.length >= 2 && path[0] === '' && path[1] === '') { + path = path.slice(1); + } + + if (path.length > 1 && path[path.length - 1] === '') { + path = path.slice(path.length - 1); + } + + function navigate(event: React.MouseEvent) { + const { button, ctrlKey, metaKey } = event; + const target = event.target as HTMLButtonElement; + const isAnchor = target.matches("a"); + + if (!target.parentElement) + return; + const link = target.parentElement.getAttribute("data-location"); + + // Let the browser natively handle non-primary click events + // or if the control or meta (Mac) keys are pressed + // ...this lets opening in a new tab or window work by default. + if (isAnchor && link && (button === 0 && !ctrlKey && !metaKey)) { + event.preventDefault(); + cockpit.location.go("/", { path: encodeURIComponent(link) }); + } + } + + return ( + navigate(event)}> + {path.map((dir, i) => { + const url_path = path.slice(0, i + 1).join("/") || '/'; + // We can't use a relative path as that will use the iframe's + // url while we want the outer shell url. And we can't obtain + // the full path of the shell easily, so a middle click will + // open a files page without the shell. + const link = `${window.location.pathname}#/?path=${url_path}`; + + return ( + + {i === 0 && + + + } + {i !== 0 && dir} + + ); + })} + + ); +}; + // eslint-disable-next-line max-len export function FilesBreadcrumbs({ path }: { path: string[] }) { const [editMode, setEditMode] = React.useState(false); const [newPath, setNewPath] = React.useState(null); - const hostname = useHostname(); - - function navigate(n_parts: number) { - cockpit.location.go("/", { path: encodeURIComponent(path.slice(0, n_parts).join("/")) }); - } const handleInputKey = (event: React.KeyboardEvent) => { // Don't propogate navigation specific events @@ -248,14 +281,15 @@ export function FilesBreadcrumbs({ path }: { path: string[] }) { setEditMode(false); }; - const fullPath = path.slice(1); - fullPath.unshift(hostname || "server"); - return ( - - - - {!editMode && + + + {!editMode && + <> - {dir !== "" &&

/

} - - ); - })} - {editMode && newPath !== null && - - event.target.select()} - onKeyDown={handleInputKey} - onChange={(_event, value) => setNewPath(value)} - /> - } - - {editMode && - <> -