Skip to content

Commit

Permalink
feat(permissions) show a warning modal for users with lower permissio…
Browse files Browse the repository at this point in the history
…ns that can't read events

Signed-off-by: David Edler <[email protected]>
  • Loading branch information
edlerd committed Mar 4, 2025
1 parent 6305840 commit 6f2a45f
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 59 deletions.
1 change: 1 addition & 0 deletions src/api/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const projectEntitlements = [
"can_create_storage_volumes",
"can_delete",
"can_edit",
"can_view_events",
];

export const fetchProjects = async (
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { enablePermissionsFeature } from "util/permissions";
import type { Location } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useLoggedInUser } from "context/useLoggedInUser";
import ProjectPermissionWarning from "pages/projects/ProjectPermissionWarning";

const isSmallScreen = () => isWidthBelow(620);

Expand Down Expand Up @@ -473,6 +474,7 @@ const Navigation: FC = () => {
<></>
)}
<div className="u-truncate">{loggedInUserName}</div>
<ProjectPermissionWarning />
</div>
</SideNavigationItem>
)}
Expand Down
37 changes: 18 additions & 19 deletions src/components/NoProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,29 @@ import CustomLayout from "./CustomLayout";

const NoProject: FC = () => {
const url = location.pathname;
const project = url.startsWith("/ui/project/")
? url.split("/")[3]
: "default";
const hasProjectInUrl = url.startsWith("/ui/project/");
const project = hasProjectInUrl ? url.split("/")[3] : "default";

return (
<CustomLayout mainClassName="no-match">
<Row>
<Col size={6} className="col-start-large-4">
<h1 className="p-heading--4">Project not found</h1>
<p>
The project <code>{project}</code> is missing or you do not have
access.
<br />
If you think this is an error in our product, please{" "}
<a
href="https://github.com/canonical/lxd-ui/issues/new"
target="_blank"
rel="noopener noreferrer"
title="Report a bug"
>
Report a bug
</a>
.
</p>
{hasProjectInUrl ? (
<>
<h1 className="p-heading--4">Project not found</h1>
<p>
The project <code>{project}</code> is missing or you do not have
the <code>viewer</code> permission for it.
</p>
</>
) : (
<>
<h1 className="p-heading--4">Permission denied</h1>
<p>
You do not have permission to view any project on this server.
</p>
</>
)}
</Col>
</Row>
</CustomLayout>
Expand Down
92 changes: 52 additions & 40 deletions src/pages/profiles/ProfileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from "react";
import {
Button,
Col,
EmptyState,
Icon,
MainTable,
Notification,
Expand Down Expand Up @@ -169,6 +170,10 @@ const ProfileList: FC = () => {

const { rows: sortedRows, updateSort } = useSortTableData({ rows });

if (isLoading) {
return <Loader text="Loading profiles..." />;
}

return (
<>
<CustomLayout
Expand All @@ -185,19 +190,21 @@ const ProfileList: FC = () => {
Profiles
</HelpLink>
</PageHeader.Title>
<PageHeader.Search>
<SearchBox
className="search-box margin-right u-no-margin--bottom"
name="search-profile"
type="text"
onChange={(value) => {
setQuery(value);
}}
placeholder="Search"
value={query}
aria-label="Search"
/>
</PageHeader.Search>
{profiles.length > 0 && (
<PageHeader.Search>
<SearchBox
className="search-box margin-right u-no-margin--bottom"
name="search-profile"
type="text"
onChange={(value) => {
setQuery(value);
}}
placeholder="Search"
value={query}
aria-label="Search"
/>
</PageHeader.Search>
)}
</PageHeader.Left>
{featuresProfiles && (
<PageHeader.BaseActions>
Expand Down Expand Up @@ -226,40 +233,45 @@ const ProfileList: FC = () => {
<NotificationRow />
<Row className="no-grid-gap">
<Col size={12}>
{!isLoading && !featuresProfiles && (
{!featuresProfiles && (
<Notification severity="caution" title="Profiles disabled">
The feature has been disabled on a project level. All the
available profiles are inherited from the{" "}
<Link to="/ui/project/default/profiles">default project</Link>.
</Notification>
)}
<ScrollableTable
dependencies={[filteredProfiles, notify.notification]}
tableId="profile-table"
belowIds={["status-bar"]}
>
<TablePagination
id="pagination"
data={sortedRows}
itemName="profile"
className="u-no-margin--top"
aria-label="Table pagination control"
{profiles.length === 0 && (
<EmptyState
className="empty-state"
image={<Icon name="repository" className="empty-state-icon" />}
title="No profiles found"
>
<MainTable
id="profile-table"
headers={headers}
sortable
emptyStateMsg={
isLoading ? (
<Loader text="Loading profiles..." />
) : (
<>No profile found matching this search</>
)
}
onUpdateSort={updateSort}
/>
</TablePagination>
</ScrollableTable>
<p>There are no profiles in this project.</p>
</EmptyState>
)}
{profiles.length > 0 && (
<ScrollableTable
dependencies={[filteredProfiles, notify.notification]}
tableId="profile-table"
belowIds={["status-bar"]}
>
<TablePagination
id="pagination"
data={sortedRows}
itemName="profile"
className="u-no-margin--top"
aria-label="Table pagination control"
>
<MainTable
id="profile-table"
headers={headers}
sortable
emptyStateMsg="No profile found matching this search"
onUpdateSort={updateSort}
/>
</TablePagination>
</ScrollableTable>
)}
</Col>
</Row>
</CustomLayout>
Expand Down
71 changes: 71 additions & 0 deletions src/pages/projects/ProjectPermissionWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button, Icon, Modal, usePortal } from "@canonical/react-components";
import { useCurrentProject } from "context/useCurrentProject";
import { useProjectEntitlements } from "util/entitlements/projects";
import { useEffect } from "react";
import { useSettings } from "context/useSettings";

const ProjectPermissionWarning = () => {
const { isLoading: isSettingsLoading } = useSettings();
const { project, isLoading: isProjectLoading } = useCurrentProject();
const { canViewEvents } = useProjectEntitlements();
const { openPortal, closePortal, isOpen, Portal } = usePortal({
programmaticallyOpen: true,
});

const hasWarning =
!isProjectLoading && !isSettingsLoading && !canViewEvents(project);

useEffect(() => {
if (hasWarning) {
openPortal();
}
}, [hasWarning]);

if (!hasWarning) {
return null;
}

return (
<div>
<Button
onClick={() => {
openPortal();
}}
appearance="base"
className="u-no-margin--bottom"
title="Missing viewer permission"
>
<Icon name="warning" className="is-light" />
</Button>
{isOpen && (
<Portal>
<Modal title="Missing viewer permission" close={closePortal}>
<p>
You do not have the <code>viewer</code> permission for the
selected project.
</p>
<p>
Changes made in the UI will not be visible without manual reload
of the page.
</p>
<p>
Your UI experience is limited because of that. Contact your
administrator to add the necessary permissions for your user.
</p>
<footer className="p-modal__footer" id="modal-footer">
<Button
appearance="positive"
className="u-no-margin--bottom"
onClick={closePortal}
>
Accept
</Button>
</footer>
</Modal>
</Portal>
)}
</div>
);
};

export default ProjectPermissionWarning;
1 change: 1 addition & 0 deletions src/pages/warnings/WarningList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const WarningList: FC = () => {
} = useQuery({
queryKey: [queryKeys.warnings],
queryFn: fetchWarnings,
retry: false, // the api returns a 403 for users with limited permissions, surface the error right away
});

if (error) {
Expand Down
8 changes: 8 additions & 0 deletions src/util/entitlements/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export const useProjectEntitlements = () => {
const canEditProject = (project?: LxdProject) =>
hasEntitlement(isFineGrained, "can_edit", project?.access_entitlements);

const canViewEvents = (project?: LxdProject) =>
hasEntitlement(
isFineGrained,
"can_view_events",
project?.access_entitlements,
);

return {
canCreateImageAliases,
canCreateImages,
Expand All @@ -62,5 +69,6 @@ export const useProjectEntitlements = () => {
canCreateStorageVolumes,
canDeleteProject,
canEditProject,
canViewEvents,
};
};

0 comments on commit 6f2a45f

Please sign in to comment.