Skip to content

Commit

Permalink
wip: alerts with buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly committed Dec 10, 2024
1 parent 657eaa2 commit b9050b4
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 29 deletions.
24 changes: 19 additions & 5 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ interface Alert {
key: string,
title: string,
variant: AlertVariant,
detail?: string,
detail?: string | React.ReactNode,
actionLinks?: React.ReactNode
}

export interface FolderFileInfo extends FileInfo {
Expand All @@ -56,12 +57,15 @@ export interface FolderFileInfo extends FileInfo {
}

interface FilesContextType {
addAlert: (title: string, variant: AlertVariant, key: string, detail?: string) => void,
addAlert: (title: string, variant: AlertVariant, key: string, detail?: string | React.ReactNode,
actionLinks?: React.ReactNode) => void,
removeAlert: (key: string) => void,
cwdInfo: FileInfo | null,
}

export const FilesContext = React.createContext({
addAlert: () => console.warn("FilesContext not initialized"),
removeAlert: () => console.warn("FilesContext not initialized"),
cwdInfo: null,
} as FilesContextType);

Expand Down Expand Up @@ -163,14 +167,23 @@ export const Application = () => {
if (loading)
return <EmptyStatePanel loading />;

const addAlert = (title: string, variant: AlertVariant, key: string, detail?: string) => {
setAlerts(prevAlerts => [...prevAlerts, { title, variant, key, ...detail && { detail }, }]);
const addAlert = (title: string, variant: AlertVariant, key: string, detail?: string | React.ReactNode,
actionLinks?: React.ReactNode) => {
setAlerts(prevAlerts => [
...prevAlerts, {
title,
variant,
key,
...detail && { detail },
...actionLinks && { actionLinks },
}
]);
};
const removeAlert = (key: string) => setAlerts(prevAlerts => prevAlerts.filter(alert => alert.key !== key));

return (
<Page>
<FilesContext.Provider value={{ addAlert, cwdInfo }}>
<FilesContext.Provider value={{ addAlert, removeAlert, cwdInfo }}>
<WithDialogs>
<AlertGroup isToast isLiveRegion>
{alerts.map(alert => (
Expand All @@ -184,6 +197,7 @@ export const Application = () => {
onClose={() => removeAlert(alert.key)}
/>
}
{...alert.actionLinks && { actionLinks: alert.actionLinks }}
key={alert.key}
>
{alert.detail}
Expand Down
3 changes: 3 additions & 0 deletions src/upload-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ button.cancel-button {
}
}

.ct-grey-text {
color: var(--pf-v5-global--Color--200);
}
62 changes: 53 additions & 9 deletions src/upload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import React, { useState, useRef } from "react";

import { AlertVariant } from "@patternfly/react-core/dist/esm/components/Alert";
import { AlertVariant, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
Expand All @@ -38,7 +38,8 @@ import { superuser } from "superuser";
import * as timeformat from "timeformat";
import { fmt_to_fragments } from "utils";

import { useFilesContext } from "./app.tsx";
import { FolderFileInfo, useFilesContext } from "./app.tsx";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import FolderFileInfo.
import { edit_permissions } from "./dialogs/permissions.tsx";
import { get_owner_candidates } from "./ownership.tsx";

import "./upload-button.scss";
Expand All @@ -51,6 +52,22 @@ interface ConflictResult {
applyToAll: boolean;
}

const UploadedFilesList = ({
files,
owner,
}: {
files: File[],
owner: string,
}) => {
const title = files.length === 1 ? files[0].name : cockpit.format(_("$0 files"), files.length);
return (
<>
<p>{title}</p>
{owner && <p className="ct-grey-text">{cockpit.format(_("Uploaded as $0"), owner)}</p>}
</>
);
};

const FileConflictDialog = ({
path,
file,
Expand Down Expand Up @@ -134,7 +151,7 @@ export const UploadButton = ({
path: string,
}) => {
const ref = useRef<HTMLInputElement>(null);
const { addAlert, cwdInfo } = useFilesContext();
const { addAlert, removeAlert, cwdInfo } = useFilesContext();
const dialogs = useDialogs();
const [showPopover, setPopover] = React.useState(false);
const [user, setUser] = useState<cockpit.UserInfo| undefined>();
Expand Down Expand Up @@ -164,7 +181,7 @@ export const UploadButton = ({
cockpit.assert(cwdInfo?.entries, "cwdInfo.entries is undefined");
let next_progress = 0;
let owner = null;
const toUploadFiles = [];
const toUploadFiles: File[] = [];

// When we are superuser upload as the owner of the directory and allow
// the user to later change ownership if required.
Expand Down Expand Up @@ -226,6 +243,7 @@ export const UploadButton = ({
await Promise.allSettled(toUploadFiles.map(async (file: File) => {
const destination = path + file.name;
const abort = new AbortController();
let options = { };

setUploadedFiles(oldFiles => {
return {
Expand All @@ -234,14 +252,13 @@ export const UploadButton = ({
};
});

let dest_tag = null;
if (owner !== null) {
try {
await cockpit.file(destination, { superuser: "try" }).replace(""); // TODO: cleanup might not work?
await cockpit.spawn(["chown", owner, destination], { superuser: "try" });
await cockpit.file(destination, { superuser: "try" }).read()
.then(((async (_content: string, tag: string) => {
dest_tag = tag;
.then((((_content: string, tag: string) => {
options = { superuser: "try", tag };
}) as any /* eslint-disable-line @typescript-eslint/no-explicit-any */));
} catch (exc) {
await cockpit.spawn(["rm", "-f", destination], { superuser: "require" });
Expand All @@ -253,6 +270,7 @@ export const UploadButton = ({
}
}

console.log(destination, options);
try {
await upload(destination, file, (progress) => {
const now = performance.now();
Expand All @@ -266,9 +284,14 @@ export const UploadButton = ({
[file.name]: { ...oldFile, progress },
};
});
}, abort.signal, { superuser: "try", tag: dest_tag });
}, abort.signal, options);
} catch (exc) {
cockpit.assert(exc instanceof Error, "Unknown exception type");

// Clean up touched file
if (owner) {
await cockpit.spawn(["rm", "-f", destination], { superuser: "require" });
}
if (exc instanceof DOMException && exc.name === 'AbortError') {
addAlert(_("Cancelled"), AlertVariant.warning, "upload",
cockpit.format(_("Cancelled upload of $0"), file.name));
Expand All @@ -290,7 +313,28 @@ export const UploadButton = ({

// If all uploads are cancelled, don't show an alert
if (cancelledUploads.length !== toUploadFiles.length) {
addAlert(_("Upload complete"), AlertVariant.success, "upload-success", _("Successfully uploaded file(s)"));
const title = cockpit.ngettext(_("File uploaded"), _("Files uploaded"), toUploadFiles.length);
const key = window.btoa(toUploadFiles.join(""));
let description;
let action;

if (owner !== null) {
description = (
<UploadedFilesList files={toUploadFiles} owner={owner} />
);
action = (
<AlertActionLink
onClick={() => {
removeAlert(key);
edit_permissions(dialogs, toUploadFiles, path);
}}
>
{_("Change permissions")}
</AlertActionLink>
);
}

addAlert(title, AlertVariant.success, key, description, action);
}
};

Expand Down
39 changes: 24 additions & 15 deletions test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import shlex
import subprocess
import tempfile
from pathlib import Path
from typing import List, Optional

# import Cockpit's machinery for test VMs and its browser test API
import testlib
Expand Down Expand Up @@ -2051,6 +2052,21 @@ class TestFiles(testlib.MachineCase):
def dir_file_count(directory: str) -> int:
return int(m.execute(f"find {directory} -mindepth 1 -maxdepth 1 | wc -l").strip())

def assert_upload_alert(files: List[str], owner_group: Optional[str] = None, *, close: bool = True) -> None:
if len(files) > 1:
b.wait_in_text(".pf-v5-c-alert__title", "Files uploaded")
b.wait_in_text(".pf-v5-c-alert__description", f"{len(files)} files")
else:
b.wait_in_text(".pf-v5-c-alert__title", "File uploaded")
b.wait_in_text(".pf-v5-c-alert__description", files[0])

if owner_group:
b.wait_in_text(".pf-v5-c-alert__description", f"Uploaded as {owner_group}")

if close:
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")

with tempfile.TemporaryDirectory() as tmpdir:
# Test cancelling of upload
big_file = str(Path(tmpdir) / "bigfile.img")
Expand Down Expand Up @@ -2129,9 +2145,7 @@ class TestFiles(testlib.MachineCase):
with b.wait_timeout(30):
b.wait(lambda: int(m.execute(f"ls {dest_dir} | wc -l").strip()) == len(files))

b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")
assert_upload_alert(files, "admin:admin")

# Verify ownership
filename = os.path.basename(files[0])
Expand Down Expand Up @@ -2164,10 +2178,7 @@ class TestFiles(testlib.MachineCase):
b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?")
b.click("button.pf-m-warning:contains('Replace')")
b.wait_not_present(".pf-v5-c-modal-box")

b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")
assert_upload_alert([os.path.basename(files[0])], "admin:admin")

self.assertEqual(m.execute(f"cat {dest_dir}/{filename}"),
subprocess.check_output(["cat", files[0]]).decode())
Expand Down Expand Up @@ -2211,9 +2222,7 @@ class TestFiles(testlib.MachineCase):
b.click("button.pf-m-warning:contains('Replace')")
b.wait_not_present(".pf-v5-c-modal-box")

b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")
assert_upload_alert(files, "admin:admin")

verify_uploaded_files(changed=True)

Expand All @@ -2235,7 +2244,9 @@ class TestFiles(testlib.MachineCase):
b.click("button.pf-m-secondary:contains('Keep original')")

b.wait_not_present(".pf-v5-c-modal-box")
b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.wait_in_text(".pf-v5-c-alert__title", "File uploaded")
b.wait_in_text(".pf-v5-c-alert__description", "newfile.txt")
b.wait_in_text(".pf-v5-c-alert__description", "Uploaded as admin:admin")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")

Expand All @@ -2259,9 +2270,7 @@ class TestFiles(testlib.MachineCase):
b.click("button.pf-m-secondary:contains('Keep original')")

b.wait_not_present(".pf-v5-c-modal-box")
b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")
assert_upload_alert([os.path.basename(files[-1])], "admin:admin")

self.assertEqual(m.execute(f"cat {dest_dir}/{filename}"), "new content")

Expand All @@ -2275,7 +2284,7 @@ class TestFiles(testlib.MachineCase):
b.click("#upload-file-btn")
b.upload_files("#upload-file-btn + input[type='file']", [files[-1]])

b.wait_in_text(".pf-v5-c-alert__description", "Successfully uploaded file")
b.wait_in_text(".pf-v5-c-alert__title", "File uploaded")
b.click(".pf-v5-c-alert__action button")
b.wait_not_present(".pf-v5-c-alert__action")

Expand Down

0 comments on commit b9050b4

Please sign in to comment.