Skip to content

Commit

Permalink
Merge branch 'kopia:main' into add-directory-breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
ptandler committed Apr 20, 2023
2 parents c62d5ac + 15cb257 commit 15033eb
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 176 deletions.
175 changes: 76 additions & 99 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6",
Expand Down
4 changes: 4 additions & 0 deletions push_local.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
set build_dir=%~dp0..\htmluibuild\build\
rd /s /q %build_dir%
md %build_dir%
xcopy /e %~dp0build\ %build_dir%
10 changes: 5 additions & 5 deletions src/PolicyEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export class PolicyEditor extends Component {
<Accordion.Body>
<SectionHeaderRow />
<Row>
<LabelColumn name="Ignore Files" help="List of file and directory names to ignore (specified as relative to the directory they are in and not absolute; wilcards are allowed)" />
<LabelColumn name="Ignore Files" help="List of file and directory names to ignore (specified as relative to the directory they are in and not absolute; wildcards are allowed)" />
<WideValueColumn>{StringList(this, "policy.files.ignore")}</WideValueColumn>
{EffectiveTextAreaValue(this, "files.ignore")}
</Row>
Expand Down Expand Up @@ -461,11 +461,11 @@ export class PolicyEditor extends Component {
<Accordion.Header><FontAwesomeIcon icon={faCogs} />&nbsp;Snapshot Actions</Accordion.Header>
<Accordion.Body>
<SectionHeaderRow />
{ActionRowScript(this, "actions.beforeSnapshotRoot.path", "Before Snapshot", "Script to run before snapshot")}
{ActionRowScript(this, "actions.beforeSnapshotRoot.script", "Before Snapshot", "Script to run before snapshot")}
{ActionRowTimeout(this, "actions.beforeSnapshotRoot.timeout")}
{ActionRowMode(this, "actions.beforeSnapshotRoot.mode")}
<hr />
{ActionRowScript(this, "actions.afterSnapshotRoot.path", "After Snapshot", "Script to run after snapshot")}
{ActionRowScript(this, "actions.afterSnapshotRoot.script", "After Snapshot", "Script to run after snapshot")}
{ActionRowTimeout(this, "actions.afterSnapshotRoot.timeout")}
{ActionRowMode(this, "actions.afterSnapshotRoot.mode")}
</Accordion.Body>
Expand All @@ -474,11 +474,11 @@ export class PolicyEditor extends Component {
<Accordion.Header><FontAwesomeIcon icon={faCog} />&nbsp;Folder Actions</Accordion.Header>
<Accordion.Body>
<SectionHeaderRow />
{ActionRowScript(this, "actions.beforeFolder.path", "Before Folder", "Script to run before folder")}
{ActionRowScript(this, "actions.beforeFolder.script", "Before Folder", "Script to run before folder")}
{ActionRowTimeout(this, "actions.beforeFolder.timeout")}
{ActionRowMode(this, "actions.beforeFolder.mode")}
<hr />
{ActionRowScript(this, "actions.afterFolder.path", "After Folder", "Script to run after folder")}
{ActionRowScript(this, "actions.afterFolder.script", "After Folder", "Script to run after folder")}
{ActionRowTimeout(this, "actions.afterFolder.timeout")}
{ActionRowMode(this, "actions.afterFolder.mode")}
</Accordion.Body>
Expand Down
4 changes: 2 additions & 2 deletions src/SetupRepository.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export class SetupRepository extends Component {
</Collapse>
{this.connectionErrorInfo()}
<hr />
<Button data-testid='back-button' variant="secondary" onClick={() => this.setState({ storageVerified: false })}>Back</Button>
<Button data-testid='back-button' variant="secondary" onClick={() => this.setState({ providerSettings: {}, storageVerified: false })}>Back</Button>
&nbsp;
<Button variant="success" type="submit" data-testid="submit-button">Create Repository</Button>
{this.loadingSpinner()}
Expand Down Expand Up @@ -493,7 +493,7 @@ export class SetupRepository extends Component {
</Collapse>
{this.connectionErrorInfo()}
<hr />
<Button data-testid='back-button' variant="secondary" onClick={() => this.setState({ storageVerified: false })}>Back</Button>
<Button data-testid='back-button' variant="secondary" onClick={() => this.setState({ providerSettings: {}, storageVerified: false })}>Back</Button>
&nbsp;
<Button variant="success" type="submit" data-testid="submit-button">Connect To Repository</Button>
{this.loadingSpinner()}
Expand Down
2 changes: 1 addition & 1 deletion src/TaskDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class TaskDetails extends Component {
}

summaryControl(task) {
const dur = formatDuration(task.startTime, task.endTime)
const dur = formatDuration(task.startTime, task.endTime, true)

switch (task.status) {

Expand Down
2 changes: 1 addition & 1 deletion src/TasksTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class TasksTable extends Component {
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.setState({ showKind: "All" })}>All</Dropdown.Item>
<Dropdown.Divider />
{this.state.uniqueKinds.map(k => <Dropdown.Item onClick={() => this.setState({ showKind: k })}>{k}</Dropdown.Item>)}
{this.state.uniqueKinds.map(k => <Dropdown.Item key={k} onClick={() => this.setState({ showKind: k })}>{k}</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
</Col>
Expand Down
54 changes: 31 additions & 23 deletions src/tests/uiutil.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { formatMilliseconds, separateMillisecondsIntoMagnitudes, formatMagnitude

describe("formatMilliseconds", () => {
it("uses 'XXs' format by default", () => {
expect(formatMilliseconds( 1)).toBe("0s"); // 1ms
expect(formatMilliseconds( 1_000)).toBe("1s"); // 1s
expect(formatMilliseconds( 60_000)).toBe("60s"); // 1m
expect(formatMilliseconds( 3600_000)).toBe("3600s"); // 1h
expect(formatMilliseconds(86400_000)).toBe("86400s"); // 1d
expect(formatMilliseconds( 1)).toBe("0.0s"); // 1ms
expect(formatMilliseconds( 1_000)).toBe("1.0s"); // 1s
expect(formatMilliseconds( 60_000)).toBe("60.0s"); // 1m
expect(formatMilliseconds( 3600_000)).toBe("3,600.0s"); // 1h
expect(formatMilliseconds(86400_000)).toBe("86,400.0s"); // 1d
});

it("uses multi-unit format if flag is set", () => {
expect(formatMilliseconds( 1, true)).toBe("0.0s");
expect(formatMilliseconds( 1_000, true)).toBe("1.0s");
expect(formatMilliseconds( 60_000, true)).toBe("1m 0s");
expect(formatMilliseconds( 3600_000, true)).toBe("1h 0m");
expect(formatMilliseconds(86400_000, true)).toBe("1d 0h");
expect(formatMilliseconds( 60_000, true)).toBe("1m");
expect(formatMilliseconds( 3600_000, true)).toBe("1h");
expect(formatMilliseconds(86400_000, true)).toBe("1d");
});
});

Expand Down Expand Up @@ -109,10 +109,10 @@ describe("formatMagnitudesUsingMultipleUnits", () => {

it("represents durations (1 minute <= T < 1 hour) using minutes (integer) and seconds (integer)", () => {
magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 minute 0 seconds");
expect(fn(magnitudes)).toBe("1 minute");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 0, milliseconds: 999 };
expect(fn(magnitudes)).toBe("1 minute 0 seconds");
expect(fn(magnitudes)).toBe("1 minute");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 1, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 minute 1 second");
Expand All @@ -123,44 +123,52 @@ describe("formatMagnitudesUsingMultipleUnits", () => {

it("represents durations (1 hour <= T < 1 day) using hours (integer) and minutes (integer)", () => {
magnitudes = { days: 0, hours: 1, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 hour 0 minutes");
expect(fn(magnitudes)).toBe("1 hour");

magnitudes = { days: 0, hours: 1, minutes: 0, seconds: 59, milliseconds: 999 };
expect(fn(magnitudes)).toBe("1 hour 0 minutes");
expect(fn(magnitudes)).toBe("1 hour 59 seconds");

magnitudes = { days: 0, hours: 1, minutes: 1, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 hour 1 minute");

magnitudes = { days: 0, hours: 23, minutes: 59, seconds: 59, milliseconds: 999 };
expect(fn(magnitudes)).toBe("23 hours 59 minutes");
expect(fn(magnitudes)).toBe("23 hours 59 minutes 59 seconds");
});

it("represents durations (T >= 1 day) using days (integer) and hours (integer)", () => {
it("represents durations (T >= 1 day) using multiple units", () => {
magnitudes = { days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 day 0 hours");
expect(fn(magnitudes)).toBe("1 day");

magnitudes = { days: 1, hours: 0, minutes: 59, seconds: 59, milliseconds: 999 };
expect(fn(magnitudes)).toBe("1 day 0 hours");
expect(fn(magnitudes)).toBe("1 day 59 minutes 59 seconds");

magnitudes = { days: 1, hours: 1, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 day 1 hour");

magnitudes = { days: 1, hours: 23, minutes: 59, seconds: 59, milliseconds: 999 };
expect(fn(magnitudes)).toBe("1 day 23 hours");
expect(fn(magnitudes)).toBe("1 day 23 hours 59 minutes 59 seconds");

magnitudes = { days: 7, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("7 days 0 hours"); // even a week uses units of days
expect(fn(magnitudes)).toBe("7 days"); // even a week uses units of days
});

it("uses localized number formatting", () => {
magnitudes = { days: 1000, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1,000 days");

magnitudes = { days: 1234567, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1,234,567 days");
})

it("uses correct singular vs. plural unit names", () => {
magnitudes = { days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 day 0 hours");
expect(fn(magnitudes)).toBe("1 day");

magnitudes = { days: 1, hours: 1, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 day 1 hour");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 minute 0 seconds");
expect(fn(magnitudes)).toBe("1 minute");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 1, milliseconds: 0 };
expect(fn(magnitudes)).toBe("1 minute 1 second");
Expand All @@ -174,13 +182,13 @@ describe("formatMagnitudesUsingMultipleUnits", () => {

it("uses abbreviated unit names when caller requests it", () => {
magnitudes = { days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes, true)).toBe("1d 0h");
expect(fn(magnitudes, true)).toBe("1d");

magnitudes = { days: 1, hours: 1, minutes: 0, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes, true)).toBe("1d 1h");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 0, milliseconds: 0 };
expect(fn(magnitudes, true)).toBe("1m 0s");
expect(fn(magnitudes, true)).toBe("1m");

magnitudes = { days: 0, hours: 0, minutes: 1, seconds: 1, milliseconds: 0 };
expect(fn(magnitudes, true)).toBe("1m 1s");
Expand All @@ -191,4 +199,4 @@ describe("formatMagnitudesUsingMultipleUnits", () => {
magnitudes = { days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 1 };
expect(fn(magnitudes, true)).toBe("0.0s");
});
});
});
96 changes: 52 additions & 44 deletions src/uiutil.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { Link } from 'react-router-dom';

const base10UnitPrefixes = ["", "K", "M", "G", "T"];

// locale to use for number formatting (undefined would use default locale, but we stick to EN for now)
const locale = "en-US"

function niceNumber(f) {
return (Math.round(f * 10) / 10.0) + '';
}
Expand Down Expand Up @@ -170,52 +173,56 @@ export function separateMillisecondsIntoMagnitudes(ms) {
* @returns {string} Formatted string representing the specified duration.
*/
export function formatMagnitudesUsingMultipleUnits(magnitudes, abbreviateUnits = false) {
let str;

// Define the label we will use for each unit, depending upon whether that
// unit's magnitude is `1` or not (e.g. "0 minutes" vs. "1 minute").
// Note: This object is not used in the final "else" block below.
const units = abbreviateUnits ? {
days: magnitudes.days === 1 ? "d" : "d",
hours: magnitudes.hours === 1 ? "h" : "h",
minutes: magnitudes.minutes === 1 ? "m" : "m",
seconds: magnitudes.seconds === 1 ? "s" : "s",
milliseconds: magnitudes.milliseconds === 1 ? "ms" : "ms",
days: "d",
hours: "h",
minutes: "m",
seconds: "s",
} : {
days: magnitudes.days === 1 ? " day" : " days",
hours: magnitudes.hours === 1 ? " hour" : " hours",
minutes: magnitudes.minutes === 1 ? " minute" : " minutes",
seconds: magnitudes.seconds === 1 ? " second" : " seconds",
milliseconds: magnitudes.milliseconds === 1 ? " millisecond" : " milliseconds",
};

// Format the duration, depending upon the magnitudes of its parts.
if (magnitudes.days > 0) {
str = `${magnitudes.days}${units.days} ${magnitudes.hours}${units.hours}`;
} else if (magnitudes.hours > 0) {
str = `${magnitudes.hours}${units.hours} ${magnitudes.minutes}${units.minutes}`;
} else if (magnitudes.minutes > 0) {
str = `${magnitudes.minutes}${units.minutes} ${magnitudes.seconds}${units.seconds}`;
} else if (magnitudes.seconds >= 10) {
str = `${magnitudes.seconds}${units.seconds}`;
} else {
// Combine the magnitudes into the equivalent total number of milliseconds.
const ms = (
magnitudes.milliseconds +
magnitudes.seconds * 1000 +
magnitudes.minutes * 60 * 1000 +
magnitudes.hours * 60 * 60 * 1000 +
magnitudes.days * 24 * 60 * 60 * 1000
);

// Convert into seconds and round to the nearest tenth of a second.
const parts = []
if (magnitudes.days) {
parts.push(`${magnitudes.days.toLocaleString(locale)}${units.days}`);
}
if (magnitudes.hours) {
parts.push(`${magnitudes.hours.toLocaleString(locale)}${units.hours}`);
}
if (magnitudes.minutes) {
parts.push(`${magnitudes.minutes.toLocaleString(locale)}${units.minutes}`);
}
if (!parts.length ||
magnitudes.seconds ||
(magnitudes.milliseconds && magnitudes.seconds < 10 && !parts.length)) {
// Convert seconds and ms into seconds
let seconds = magnitudes.seconds;
let fractionDigits = 0;

// add ms only if duration is < 10s
if (seconds < 10 && !parts.length) {
seconds += magnitudes.milliseconds / 1000;
fractionDigits = 1;
}

// `toFixed()` doesn't support localization, use `toLocaleString()` instead
// Given that the number always has a decimal place, use the "plural"
// unit label, even if the number is `1.0`.
const seconds = ms / 1000;
str = `${seconds.toFixed(1)}${abbreviateUnits ? "s" : " seconds"}`;
parts.push(`${seconds.toLocaleString(locale, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
roundingMode: "trunc",
})}${fractionDigits ? (abbreviateUnits ? "s" : " seconds") : units.seconds }`);
}

return str;
return parts.join(" ");
}

/**
Expand All @@ -231,30 +238,31 @@ export function formatMilliseconds(ms, useMultipleUnits = false) {
return formatMillisecondsUsingMultipleUnits(ms);
}

return Math.round(ms / 100.0) / 10.0 + "s"
// return Math.round(ms / 100.0) / 10.0 + "s"
// always show one fraction digit, to avoid layout changes every 0.5 sec for running tasks
// `toFixed()` doesn't support localization, use `toLocaleString()` instead
return (ms / 1000.0).toLocaleString(locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
}) + "s"
}

export function formatDuration(from, to, useMultipleUnits = false) {
if (!from) {
return "";
}

if (!to) {
const ms = new Date().valueOf() - new Date(from).valueOf();
if (ms < 0) {
return ""
}

return formatMilliseconds(ms)
const ms = (to ? new Date(to) : new Date()).valueOf() - new Date(from).valueOf();
if (ms < 0) {
return ""
}

return formatMilliseconds(new Date(to).valueOf() - new Date(from).valueOf(), useMultipleUnits);
return formatMilliseconds(ms, useMultipleUnits);
}

export function taskStatusSymbol(task) {
const st = task.status;
const dur = formatDuration(task.startTime, task.endTime);
const durMultiUnit = formatDuration(task.startTime, task.endTime, true);
const dur = formatDuration(task.startTime, task.endTime, true);

switch (st) {
case "RUNNING":
Expand All @@ -265,13 +273,13 @@ export function taskStatusSymbol(task) {
</>;

case "SUCCESS":
return <p title={dur}><FontAwesomeIcon icon={faCheck} color="green" /> Finished in {durMultiUnit}</p>;
return <p title={dur}><FontAwesomeIcon icon={faCheck} color="green" /> Finished in {dur}</p>;

case "FAILED":
return <p title={dur}><FontAwesomeIcon icon={faExclamationCircle} color="red" /> Failed after {durMultiUnit}</p>;
return <p title={dur}><FontAwesomeIcon icon={faExclamationCircle} color="red" /> Failed after {dur}</p>;

case "CANCELED":
return <p title={dur}><FontAwesomeIcon icon={faBan} /> Canceled after {durMultiUnit}</p>;
return <p title={dur}><FontAwesomeIcon icon={faBan} /> Canceled after {dur}</p>;

default:
return st;
Expand Down

0 comments on commit 15033eb

Please sign in to comment.