diff --git a/src/app.tsx b/src/app.tsx index 3aa58212b..1327ab92c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -17,7 +17,7 @@ * along with Cockpit; If not, see . */ -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { AlertGroup, Alert, AlertVariant, AlertActionCloseButton @@ -67,6 +67,22 @@ export const FilesContext = React.createContext({ export const useFilesContext = () => useContext(FilesContext); +export const usePath = () => { + const { options } = usePageLocation(); + let currentPath = decodeURIComponent(options.path?.toString() || "/"); + + // Our path will always be `/foo/` formatted + if (!currentPath.endsWith("/")) { + currentPath += "/"; + } + + if (!currentPath.startsWith("/")) { + currentPath = `/${currentPath}`; + } + + return currentPath; +}; + export const Application = () => { const { options } = usePageLocation(); const [loading, setLoading] = useState(true); @@ -79,10 +95,8 @@ export const Application = () => { const [alerts, setAlerts] = useState([]); const [cwdInfo, setCwdInfo] = useState(null); - const currentPath = decodeURIComponent(options.path?.toString() || ""); - // the function itself is not expensive, but `path` is later used in expensive computation - // and changing its reference value on every render causes performance issues - const path = useMemo(() => currentPath?.split("/"), [currentPath]); + const path = usePath(); + console.log("path", path); useEffect(() => { cockpit.user().then(user => { @@ -102,7 +116,7 @@ export const Application = () => { setSelected([]); const client = new FsInfoClient( - `/${currentPath}`, + path, ["type", "mode", "size", "mtime", "user", "group", "target", "entries", "targets"], { superuser: 'try' } ); @@ -126,7 +140,7 @@ export const Application = () => { client.close(); }; }, - [options, currentPath] + [options, path] ); if (loading) @@ -159,9 +173,7 @@ export const Application = () => { ))} - + diff --git a/src/common.ts b/src/common.ts index d1caaded4..d53aadfc8 100644 --- a/src/common.ts +++ b/src/common.ts @@ -53,6 +53,11 @@ export function * map_permissions(func: (value: number, label: string) => T) } export function basename(path: string) { + if (path === '/') { + return path; + } + + path = path.replace(/\/$/, ''); const elements = path.split('/'); if (elements.length === 0) { return '/'; diff --git a/src/dialogs/permissions.jsx b/src/dialogs/permissions.jsx index 64d65c255..8529ca001 100644 --- a/src/dialogs/permissions.jsx +++ b/src/dialogs/permissions.jsx @@ -32,7 +32,7 @@ import { etc_group_syntax, etc_passwd_syntax } from 'pam_user_parser'; import { superuser } from 'superuser'; import { useFilesContext } from '../app'; -import { map_permissions, inode_types } from '../common'; +import { map_permissions, inode_types, basename } from '../common'; const _ = cockpit.gettext; @@ -41,7 +41,7 @@ const EditPermissionsModal = ({ dialogResult, selected, path }) => { // Nothing selected means we act on the current working directory if (!selected) { - const directory_name = path[path.length - 1]; + const directory_name = basename(path); selected = { ...cwdInfo, isCwd: true, name: directory_name }; } @@ -82,7 +82,7 @@ const EditPermissionsModal = ({ dialogResult, selected, path }) => { const ownerChanged = owner !== selected.user || group !== selected.group; try { - const directory = selected?.isCwd ? path.join("/") : path.join("/") + "/" + selected.name; + const directory = selected?.isCwd ? path : path + selected.name; if (permissionChanged) await cockpit.spawn(["chmod", mode.toString(8), directory], { superuser: "try", err: "message" }); diff --git a/src/dialogs/rename.tsx b/src/dialogs/rename.tsx index ef29afdff..5c8bb4a7f 100644 --- a/src/dialogs/rename.tsx +++ b/src/dialogs/rename.tsx @@ -74,7 +74,7 @@ function checkCanOverride(candidate: string, entries: Record, const RenameItemModal = ({ dialogResult, path, selected } : { dialogResult: DialogResult - path: string[], + path: string, selected: FolderFileInfo, }) => { const { cwdInfo } = useFilesContext(); @@ -84,12 +84,12 @@ const RenameItemModal = ({ dialogResult, path, selected } : { const [overrideFileName, setOverrideFileName] = useState(false); const renameItem = (force = false) => { - const newPath = path.join("/") + "/" + name; + const newPath = path + name; const mvCmd = ["mv", "--no-target-directory"]; if (force) { mvCmd.push("--force"); } - mvCmd.push(path.join("/") + "/" + selected.name, newPath); + mvCmd.push(path + selected.name, newPath); cockpit.spawn(mvCmd, { superuser: "try", err: "message" }) .then(() => { @@ -167,6 +167,6 @@ const RenameItemModal = ({ dialogResult, path, selected } : { ); }; -export function show_rename_dialog(dialogs: Dialogs, path: string[], selected: FolderFileInfo) { +export function show_rename_dialog(dialogs: Dialogs, path: string, selected: FolderFileInfo) { dialogs.run(RenameItemModal, { path, selected }); } diff --git a/src/files-breadcrumbs.tsx b/src/files-breadcrumbs.tsx index c7710fa0d..eb834589d 100644 --- a/src/files-breadcrumbs.tsx +++ b/src/files-breadcrumbs.tsx @@ -60,7 +60,7 @@ function useHostname() { return hostname; } -function BookmarkButton({ path }: { path: string[] }) { +function BookmarkButton({ path }: { path: string }) { const [isOpen, setIsOpen] = React.useState(false); const [user, setUser] = React.useState(null); const [bookmarks, setBookmarks] = React.useState([]); @@ -68,7 +68,6 @@ function BookmarkButton({ path }: { path: string[] }) { const { addAlert, cwdInfo } = useFilesContext(); - const currentPath = path.join("/") || "/"; const defaultBookmarks = []; if (user?.home) defaultBookmarks.push({ name: _("Home"), loc: user?.home }); // TODO: add trash @@ -122,11 +121,11 @@ function BookmarkButton({ path }: { path: string[] }) { try { await bookmarkHandle.modify((old_content: string) => { - if (bookmarks.includes(currentPath)) { - return old_content.split('\n').filter(line => parse_uri(line) !== currentPath) + if (bookmarks.includes(path)) { + return old_content.split('\n').filter(line => parse_uri(line) !== path) .join('\n'); } else { - const newBoomark = "file://" + path.map(part => encodeURIComponent(part)) + const newBoomark = "file://" + path.split('/').map(part => encodeURIComponent(part)) .join('/') + "\n"; return (old_content || '') + newBoomark; } @@ -152,8 +151,8 @@ function BookmarkButton({ path }: { path: string[] }) { return null; let actionText = null; - if (currentPath !== user.home) { - if (bookmarks.includes(currentPath)) { + if (path !== user.home) { + if (bookmarks.includes(path)) { actionText = _("Remove current directory"); } else if (cwdInfo !== null) { actionText = _("Add bookmark"); @@ -206,13 +205,16 @@ function BookmarkButton({ path }: { path: string[] }) { } // eslint-disable-next-line max-len -export function FilesBreadcrumbs({ path }: { path: string[] }) { +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("/")) }); + cockpit.location.go("/", { + path: encodeURIComponent(path.split('/').slice(0, n_parts) + .join("/")) + }); } const handleInputKey = (event: React.KeyboardEvent) => { @@ -232,7 +234,7 @@ export function FilesBreadcrumbs({ path }: { path: string[] }) { const enableEditMode = () => { setEditMode(true); - setNewPath(path.join("/") || "/"); + setNewPath(path); }; const changePath = () => { @@ -248,8 +250,12 @@ export function FilesBreadcrumbs({ path }: { path: string[] }) { setEditMode(false); }; - const fullPath = path.slice(1); + const fullPath = path.split('/').slice(1, path.length); fullPath.unshift(hostname || "server"); + // HACK: drop the last /, as the breadcrumb already inserts an artifical / + if (fullPath.length > 1) { + fullPath.pop(); + } return ( @@ -268,7 +274,7 @@ export function FilesBreadcrumbs({ path }: { path: string[] }) { return (