Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a0a4e2f
feat: redesign the reports side panel on the V2 endpoint
yuriy-vasilyev Jun 17, 2026
4c615d2
initial tsv exports
rubinaga Jun 9, 2026
2d3f4ba
refine
rubinaga Jun 16, 2026
1530d37
add UI fixes
rubinaga Jun 17, 2026
bc57fc2
improve tests
rubinaga Jun 18, 2026
a81ddb6
add changeset
rubinaga Jun 18, 2026
c077098
copilot fixes
rubinaga Jun 18, 2026
aaf0eda
add more copilot fixes
rubinaga Jun 19, 2026
8e1dcb3
add retry button
rubinaga Jun 24, 2026
fc590c8
prettier fixes
rubinaga Jun 24, 2026
d8993cc
change export id to number
rubinaga Jun 24, 2026
594067b
fix eslint
rubinaga Jun 24, 2026
3d084c6
Merge branch 'main' into feat/reports-tsv-export
marqode Jun 24, 2026
33b0929
Merge branch 'main' into feat/reports-tsv-export
marqode Jun 24, 2026
14ea3c9
merge in tsv-exports
marqode Jun 24, 2026
a8f9c1f
add tsv export form and mock handler for reports
marqode Jun 24, 2026
edb8148
add tests for report export
marqode Jun 24, 2026
ef0e2c1
add changeset
marqode Jun 24, 2026
c707b3a
run prettier, fix lint error
marqode Jun 24, 2026
c49d5e2
integrate changes from other feature branches, add tests
marqode Jun 24, 2026
48186d9
remove extra changeset
marqode Jun 24, 2026
ed390b2
Apply suggestions from code review
marqode Jun 24, 2026
a79cbdf
add Other tooltip to form, close side panel on export download
marqode Jun 24, 2026
1adc3aa
remove dead code
marqode Jun 24, 2026
d2bc0ac
Apply suggestions from code review
marqode Jun 24, 2026
81d87e6
change progressbar design
rubinaga Jun 25, 2026
e4866d8
prettier fixes
rubinaga Jun 25, 2026
14fe183
add copilot suggestions
rubinaga Jun 25, 2026
811fb23
update by_cve report export format
marqode Jun 25, 2026
a6a5d99
update form and detail view with description field, fields for by_cve…
marqode Jun 26, 2026
6490fb2
merge feature/tsv-exports
marqode Jun 26, 2026
8f2da0e
Apply suggestions from code review
marqode Jun 26, 2026
d54bbed
merge main, fix failing test
marqode Jun 26, 2026
0442bbf
update description field
marqode Jun 26, 2026
1fb0957
fix failing tests
marqode Jun 26, 2026
5f40b9e
run prettier
marqode Jun 26, 2026
29ace65
Apply suggestions from code review
marqode Jun 26, 2026
3beae26
update exportsList label and tests
marqode Jun 26, 2026
5565745
update sample description
marqode Jun 26, 2026
ec88683
add cve fields to default selection for instance compliance reports
marqode Jun 29, 2026
7d2759b
Merge branch 'main' into feat/reports-tsv-export
marqode Jun 29, 2026
c448b7b
display primary identity group before compliance in report export fields
marqode Jun 29, 2026
54837eb
merge in field updates, fix tests
marqode Jun 29, 2026
acbb008
restore empty changeset from main
marqode Jun 30, 2026
ab9f51d
fix failing test
marqode Jun 30, 2026
43a7e4a
fix: pagination for mirrors (#686)
marqode Jun 30, 2026
87552f2
feat: restore LRO Mirrors and Publications onto main (#693)
yurii-vasyliev Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/better-boats-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": minor
---

Add publishing status for publications
5 changes: 5 additions & 0 deletions .changeset/early-mice-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": patch
---

Fix pagination for mirrors when number of mirrors is >20
5 changes: 5 additions & 0 deletions .changeset/fuzzy-toes-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": minor
---

Add syncing status for mirrors
5 changes: 5 additions & 0 deletions .changeset/ripe-planets-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": minor
---

Add tsv export feature using v2 endpoint for instance reports
1 change: 0 additions & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ VITE_API_URL_OLD=
VITE_API_URL_DEB_ARCHIVE=
VITE_ROOT_PATH=
VITE_SELF_HOSTED_ENV=
VITE_REPORT_VIEW_ENABLED=
VITE_DETAILED_UPGRADES_VIEW_ENABLED=
VITE_MSW_ENABLED=
VITE_MSW_ENDPOINTS_TO_INTERCEPT=
Expand Down
1 change: 0 additions & 1 deletion .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ VITE_API_URL=/api/v2/
VITE_API_URL_OLD=/api/
VITE_API_URL_DEB_ARCHIVE=/debarchive/v1beta1/
VITE_ROOT_PATH=/new_dashboard/
VITE_REPORT_VIEW_ENABLED=false
VITE_DETAILED_UPGRADES_VIEW_ENABLED=false
VITE_MSW_ENABLED=false
VITE_MSW_ENDPOINTS_TO_INTERCEPT=
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dist-ssr
.venv
.vite-node
coverage
reports
/reports
src/**/*.module.scss.d.ts

# Cache
Expand Down
48 changes: 48 additions & 0 deletions src/components/ProgressBar/ProgressBar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@import "vanilla-framework/scss/settings_colors";

.wrapper {
align-items: center;
display: flex;
width: 100%;
}

.content {
align-items: center;
display: flex;
flex: 1 1 auto;
gap: 0.5rem;
min-width: 0;
}

.fullWidth {
.bar {
max-width: none;
}
}

.bar {
background-color: $color-mid-x-light;
flex: 1 1 auto;
height: 0.5rem;
max-width: 8rem;
min-width: 0;
overflow: hidden;

.fill {
background-color: $colors--theme--link-default;
height: 100%;
transition: width 0.3s ease;
}
}

.percentage {
flex: 0 0 auto;
}

.eta {
color: $colors--theme--text-muted;
flex: 0 0 6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
33 changes: 33 additions & 0 deletions src/components/ProgressBar/ProgressBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import ProgressBar from "./ProgressBar";

describe("ProgressBar", () => {
it("renders the progress percentage", () => {
render(<ProgressBar progress={50} secondsRemaining={null} />);
expect(screen.getByText("50%")).toBeInTheDocument();
});

it("renders the ETA label", () => {
render(<ProgressBar progress={50} secondsRemaining={120} />);
expect(screen.getByText("2m")).toBeInTheDocument();
});

it("renders a progress bar with correct aria attributes", () => {
render(<ProgressBar progress={75} secondsRemaining={null} />);
const progressbar = screen.getByRole("progressbar");
expect(progressbar).toHaveAttribute("aria-valuenow", "75");
expect(progressbar).toHaveAttribute("aria-valuemin", "0");
expect(progressbar).toHaveAttribute("aria-valuemax", "100");
});

it("clamps progress to 0-100", () => {
render(<ProgressBar progress={150} secondsRemaining={null} />);
expect(screen.getByText("100%")).toBeInTheDocument();
});

it("rounds progress to nearest integer", () => {
render(<ProgressBar progress={33.7} secondsRemaining={null} />);
expect(screen.getByText("34%")).toBeInTheDocument();
});
});
65 changes: 65 additions & 0 deletions src/components/ProgressBar/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { FC } from "react";
import { MAX_PROGRESS } from "./constants";
import { getEtaLabel } from "./helpers";
import classes from "./ProgressBar.module.scss";
import classNames from "classnames";

export interface ProgressBarProps {
readonly progress: number;
readonly secondsRemaining: number | null;
readonly fullWidth?: boolean;
readonly loading?: boolean;
}

const ProgressBar: FC<ProgressBarProps> = ({
progress,
secondsRemaining,
fullWidth = false,
loading = false,
}) => {
const clampedProgress = Math.min(
MAX_PROGRESS,
Math.max(0, Math.round(progress)),
);

return (
<div className={`${classes.wrapper} ${fullWidth ? classes.fullWidth : ""}`}>
{loading && (
<i className="p-icon--spinner u-animation--spin" aria-hidden="true" />
)}
<div className={classes.content}>
<div
className={classes.bar}
role="progressbar"
aria-valuenow={clampedProgress}
aria-valuemin={0}
aria-valuemax={MAX_PROGRESS}
aria-label="Progress"
>
<div
className={classes.fill}
style={{ width: `${clampedProgress}%` }}
/>
</div>
<span
className={classNames(
classes.percentage,
"p-text--small u-no-margin--bottom",
)}
>
{clampedProgress}%
</span>
<span
className={classNames(
classes.eta,
"p-text--small u-no-margin--bottom",
)}
>
{getEtaLabel(secondsRemaining)}
</span>
</div>
</div>
);
};

export default ProgressBar;
4 changes: 4 additions & 0 deletions src/components/ProgressBar/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const SECONDS_PER_MINUTE = 60;
export const SECONDS_PER_HOUR = 3600;
export const ALMOST_DONE_THRESHOLD_SECONDS = 5;
export const MAX_PROGRESS = 100;
47 changes: 47 additions & 0 deletions src/components/ProgressBar/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint @typescript-eslint/no-magic-numbers: 0 */
import { describe, it, expect } from "vitest";
import { formatSecondsRemaining, getEtaLabel } from "./helpers";

describe("formatSecondsRemaining", () => {
it('formats seconds as "Xs" when under a minute', () => {
expect(formatSecondsRemaining(30)).toBe("30s");
});

it("rounds to the nearest second", () => {
expect(formatSecondsRemaining(30.7)).toBe("31s");
});

it("clamps to zero for negative values", () => {
expect(formatSecondsRemaining(-5)).toBe("0s");
});

it('formats as "Xm" for whole minutes', () => {
expect(formatSecondsRemaining(120)).toBe("2m");
});

it('formats as "Xm Ys" for minutes with remainder', () => {
expect(formatSecondsRemaining(150)).toBe("2m 30s");
});

it('formats as "Xh Ym" for hours with remainder minutes', () => {
expect(formatSecondsRemaining(19800)).toBe("5h 30m");
});

it('formats as "Xh" for whole hours', () => {
expect(formatSecondsRemaining(7200)).toBe("2h");
});
});

describe("getEtaLabel", () => {
it('returns "Estimating..." when secondsRemaining is null', () => {
expect(getEtaLabel(null)).toBe("Estimating...");
});

it('returns "Almost done" when within threshold', () => {
expect(getEtaLabel(3)).toBe("Almost done");
});

it("delegates to formatSecondsRemaining for larger values", () => {
expect(getEtaLabel(120)).toBe("2m");
});
});
39 changes: 39 additions & 0 deletions src/components/ProgressBar/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ALMOST_DONE_THRESHOLD_SECONDS,
SECONDS_PER_HOUR,
SECONDS_PER_MINUTE,
} from "./constants";

export const formatSecondsRemaining = (seconds: number): string => {
const safe = Math.max(0, Math.round(seconds));

if (safe >= SECONDS_PER_HOUR) {
const hours = Math.floor(safe / SECONDS_PER_HOUR);
const minutes = Math.floor((safe % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);

return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
}

if (safe >= SECONDS_PER_MINUTE) {
const minutes = Math.floor(safe / SECONDS_PER_MINUTE);
const remainderSeconds = safe % SECONDS_PER_MINUTE;

return remainderSeconds
? `${minutes}m ${remainderSeconds}s`
: `${minutes}m`;
}

return `${safe}s`;
};

export const getEtaLabel = (secondsRemaining: number | null): string => {
if (secondsRemaining === null) {
return "Estimating...";
}

if (secondsRemaining <= ALMOST_DONE_THRESHOLD_SECONDS) {
return "Almost done";
}

return formatSecondsRemaining(secondsRemaining);
};
1 change: 1 addition & 0 deletions src/components/ProgressBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./ProgressBar";
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface TablePaginationProps {
readonly className?: string;
readonly handleClearSelection?: () => void;
readonly currentItemCount?: number;
readonly pageSizeLabel?: string;
}

const TablePagination: FC<TablePaginationProps> = ({
totalItems,
className = "",
currentItemCount = 0,
handleClearSelection,
pageSizeLabel,
}) => {
const { currentPage, pageSize, setPageParams } = usePageParams();
const totalPages = useTotalPages(totalItems, pageSize);
Expand All @@ -39,8 +41,8 @@ const TablePagination: FC<TablePaginationProps> = ({
}
};

const setPageSize = (pageSize: number) => {
setPageParams({ pageSize });
const setPageSize = (newPageSize: number) => {
setPageParams({ pageSize: newPageSize });
};

return (
Expand All @@ -52,6 +54,7 @@ const TablePagination: FC<TablePaginationProps> = ({
paginate={paginate}
setPageSize={setPageSize}
totalItems={totalItems}
pageSizeLabel={pageSizeLabel}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("TablePaginationBase", () => {
it("should change the page size", async () => {
render(<TablePaginationBase {...props} currentPage={2} />);

const pageSizeOption = PAGE_SIZE_OPTIONS[1];
const [, pageSizeOption] = PAGE_SIZE_OPTIONS;

await userEvent.selectOptions(
screen.getByRole("combobox", { name: "Instances per page" }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface TablePaginationBaseProps {
readonly paginate: (page: number) => void;
readonly setPageSize: (itemsNumber: number) => void;
readonly totalItems?: number | undefined;
readonly pageSizeLabel?: string;
}

const TablePaginationBase: FC<TablePaginationBaseProps> = ({
Expand All @@ -25,6 +26,7 @@ const TablePaginationBase: FC<TablePaginationBaseProps> = ({
paginate,
setPageSize,
currentPage,
pageSizeLabel = "Instances per page",
}) => {
const totalPages = useTotalPages(totalItems, pageSize);

Expand Down Expand Up @@ -68,7 +70,7 @@ const TablePaginationBase: FC<TablePaginationBaseProps> = ({
<div className={classes.paginationContainer}>
<Select
name="pageSize"
label="Instances per page"
label={pageSizeLabel}
labelClassName="u-off-screen"
className="u-no-margin--bottom"
options={PAGE_SIZE_OPTIONS}
Expand Down
3 changes: 1 addition & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const NOT_AVAILABLE = "N/A";
export const APP_VERSION = import.meta.env.VITE_APP_VERSION;
export const APP_COMMIT = import.meta.env.VITE_APP_COMMIT;
export const FEEDBACK_LINK = "https://bugs.launchpad.net/landscape";
export const REPORT_VIEW_ENABLED =
import.meta.env.VITE_REPORT_VIEW_ENABLED === "true";
export const CONTACT_SUPPORT_TEAM_MESSAGE =
"Something went wrong. Please try again or contact our support team.";
export const DETAILED_UPGRADES_VIEW_ENABLED =
Expand Down Expand Up @@ -45,3 +43,4 @@ export const MAX_PASSWORD_LENGTH = 50;
export const DEFAULT_MODAL_PAGE_SIZE = 10;
export const GENERIC_DOMAIN = "landscape.canonical.com";
export const MASKED_VALUE = "****************";
export const DEFAULT_POLLING_INTERVAL = 2000;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const useCreateStandaloneAccount = () => {
const axiosInstance = axios.create({ baseURL: API_URL });

const { isPending, mutateAsync } = useMutation<
AxiosResponse<unknown>,
AxiosResponse<void>,
AxiosError<ApiError>,
CreateStandaloneAccountParams
>({
Expand Down
1 change: 1 addition & 0 deletions src/features/activities/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as useCancelActivities } from "./useCancelActivities";
export { default as useGetActivityTypes } from "./useGetActivityTypes";
export { default as useGetSingleActivity } from "./useGetSingleActivity";
export { default as useRedoActivities } from "./useRedoActivities";
export { useExportActivitiesTsv } from "./useExportActivitiesTsv";
Loading
Loading