Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8957771
fix: retrack PR2 based on PR1
haydnli-shopify Jun 10, 2025
ef9e0d8
fix: lint errors in test_projects.py
haydnli-shopify Jun 10, 2025
f91bf86
fix: update test assertions to include is_public field
haydnli-shopify Jun 10, 2025
f83e3c9
style: apply pre-commit formatting fixes
haydnli-shopify Jun 10, 2025
37fe42c
Add project visibility update API endpoint
haydnli-shopify Jun 11, 2025
846eb09
Fix user autocomplete: allow all users to see user list
haydnli-shopify Jun 11, 2025
3d0ea5b
Fix project deletion permissions: allow project owners to delete thei…
haydnli-shopify Jun 12, 2025
52d9212
Apply pre-commit formatting fixes to PR2
haydnli-shopify Jun 12, 2025
e515981
Fix project deletion permissions and member race conditions
haydnli-shopify Jun 12, 2025
e3f6ea0
Allow public project access to gateways endpoints
haydnli-shopify Jun 12, 2025
36967fe
fix: address PR comments from r4victor
haydnli-shopify Jun 17, 2025
beaa33c
feat: generalize project update endpoint
haydnli-shopify Jun 23, 2025
9e4efaf
refactor: rename /update_visibility to /update for more generic proje…
haydnli-shopify Jun 23, 2025
b589cf4
Remove redundant comments that duplicate code logic
haydnli-shopify Jun 25, 2025
f61f416
Remove only redundant comments added in PR2
haydnli-shopify Jun 25, 2025
79920ee
Add join/leave project UI functionality
haydnli-shopify Jun 12, 2025
6df964e
Add Join/Leave button to project header and restrict CLI section to m…
haydnli-shopify Jun 12, 2025
8be83d4
refactor: improve code style and extract member actions hook
haydnli-shopify Jun 18, 2025
7722672
refactor: use useProjectMemberActions in ProjectDetails
haydnli-shopify Jun 18, 2025
b0bfc08
Add join/leave project UI functionality
haydnli-shopify Jun 12, 2025
7edf748
Add Join/Leave button to project header and restrict CLI section to m…
haydnli-shopify Jun 12, 2025
427196c
refactor: improve code style and extract member actions hook
haydnli-shopify Jun 18, 2025
8c8379f
refactor: use useProjectMemberActions in ProjectDetails
haydnli-shopify Jun 18, 2025
1f425bc
Fix linter errors and remove obsolete updateProjectVisibility mutation
haydnli-shopify Jun 23, 2025
8fe5267
Move project visibility to Danger Zone with ComboBox and add visibili…
haydnli-shopify Jun 23, 2025
06f9133
Fix: Hide 'Leave Project' button for project owners
haydnli-shopify Jun 23, 2025
863f702
Hide Leave Project button for last admin instead of showing error
haydnli-shopify Jun 23, 2025
cb13d4f
Fix project visibility display issue
haydnli-shopify Jun 23, 2025
e8a58a7
Minor changes:
peterschmidt85 Jun 26, 2025
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
3 changes: 3 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export const API = {
DETAILS: (name: IProject['project_name']) => `${API.PROJECTS.BASE()}/${name}`,
DETAILS_INFO: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/get`,
SET_MEMBERS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/set_members`,
ADD_MEMBERS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/add_members`,
REMOVE_MEMBERS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/remove_members`,
UPDATE: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/update`,

// Repos
REPOS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/repos`,
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/ButtonWithConfirmation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { ConfirmationDialog } from '../ConfirmationDialog';

import { IProps } from './types';

export const ButtonWithConfirmation: React.FC<IProps> = ({ confirmTitle, confirmContent, onClick, ...props }) => {
export const ButtonWithConfirmation: React.FC<IProps> = ({
confirmTitle,
confirmContent,
onClick,
confirmButtonLabel,
...props
}) => {
const [showDeleteConfirm, setShowConfirmDelete] = useState(false);

const toggleDeleteConfirm = () => {
Expand All @@ -31,6 +37,7 @@ export const ButtonWithConfirmation: React.FC<IProps> = ({ confirmTitle, confirm
onConfirm={onConfirm}
title={confirmTitle}
content={content}
confirmButtonLabel={confirmButtonLabel}
/>
</>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ButtonWithConfirmation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import type { IProps as ButtonProps } from '../Button';
export interface IProps extends Omit<ButtonProps, 'onClick'> {
confirmTitle?: string;
confirmContent?: ReactNode;
confirmButtonLabel?: string;
onClick?: () => void;
}
21 changes: 21 additions & 0 deletions frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@
"runs": "Runs",
"tags": "Tags",
"settings": "Settings",
"join": "Join",
"leave_confirm_title": "Leave project",
"leave_confirm_message": "Are you sure you want to leave this project?",
"leave": "Leave",
"join_success": "Successfully joined the project",
"leave_success": "Successfully left the project",
"join_error": "Failed to join project",
"leave_error": "Failed to leave project",
"card": {
"backend": "Backend",
"settings": "Settings"
Expand All @@ -181,6 +189,8 @@
"project_name": "Project name",
"owner": "Owner",
"project_name_description": "Only latin characters, dashes, underscores, and digits",
"is_public": "Make project public",
"is_public_description": "Public projects can be accessed by any user without being a member",
"backend": "Backend",
"backend_config": "Backend config",
"backend_config_description": "Specify the backend config in the YAML format. Click Info for examples.",
Expand All @@ -189,6 +199,13 @@
"members_empty_message_title": "No members",
"members_empty_message_text": "Select project's members",
"update_members_success": "Members are updated",
"update_visibility_success": "Project visibility updated successfully",
"update_visibility_confirm_title": "Change project visibility",
"update_visibility_confirm_message": "Are you sure you want to change the project visibility? This will affect who can access this project.",
"change_visibility": "Change visibility",
"project_visibility": "Project visibility",
"project_visibility_description": "Control who can access this project",
"make_project_public": "Make project public",
"delete_project_confirm_title": "Delete project",
"delete_project_confirm_message": "Are you sure you want to delete this project?",
"delete_projects_confirm_title": "Delete projects",
Expand Down Expand Up @@ -282,6 +299,10 @@
"error_notification": "Update project error",
"validation": {
"user_name_format": "Only letters, numbers, - or _"
},
"visibility": {
"private": "Private",
"public": "Public"
}
},
"create": {
Expand Down
170 changes: 127 additions & 43 deletions frontend/src/pages/Project/Details/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { debounce } from 'lodash';
Expand All @@ -17,18 +17,21 @@ import {
SelectCSD,
SpaceBetween,
StatusIndicator,
Toggle,
} from 'components';
import { HotspotIds } from 'layouts/AppLayout/TutorialPanel/constants';

import { useBreadcrumbs, useHelpPanel, useNotifications } from 'hooks';
import { riseRouterException } from 'libs';
import { ROUTES } from 'routes';
import { useGetProjectQuery, useUpdateProjectMembersMutation } from 'services/project';
import { useGetProjectQuery, useUpdateProjectMembersMutation, useUpdateProjectMutation } from 'services/project';

import { useCheckAvailableProjectPermission } from 'pages/Project/hooks/useCheckAvailableProjectPermission';
import { useConfigProjectCliCommand } from 'pages/Project/hooks/useConfigProjectCliComand';
import { useDeleteProject } from 'pages/Project/hooks/useDeleteProject';
import { ProjectMembers } from 'pages/Project/Members';
import { getProjectRoleByUserName } from 'pages/Project/utils';
import { useGetUserDataQuery } from 'services/user';

import { useBackendsTable } from '../../Backends/hooks';
import { BackendsTable } from '../../Backends/Table';
Expand All @@ -51,23 +54,40 @@ export const ProjectSettings: React.FC = () => {

const [pushNotification] = useNotifications();
const [updateProjectMembers] = useUpdateProjectMembersMutation();
const [updateProject] = useUpdateProjectMutation();
const { deleteProject, isDeleting } = useDeleteProject();
const { data: currentUser } = useGetUserDataQuery({});

const { data, isLoading, error } = useGetProjectQuery({ name: paramProjectName });

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error?.status === 404) {
if (error && 'status' in error && error.status === 404) {
riseRouterException();
}
}, [error]);

const currentUserRole = data ? getProjectRoleByUserName(data, currentUser?.username ?? '') : null;
const isProjectMember = currentUserRole !== null;

const currentOwner = {
label: data?.owner.username,
value: data?.owner.username,
};

const visibilityOptions = [
{ label: t('projects.edit.visibility.private') || '', value: 'private' },
{ label: t('projects.edit.visibility.public') || '', value: 'public' },
];

const currentVisibility = data?.isPublic ? 'public' : 'private';
const [selectedVisibility, setSelectedVisibility] = useState(
data?.isPublic ? visibilityOptions[1] : visibilityOptions[0]
);

useEffect(() => {
setSelectedVisibility(data?.isPublic ? visibilityOptions[1] : visibilityOptions[0]);
}, [data]);

const {
data: backendsData,
isDeleting: isDeletingBackend,
Expand Down Expand Up @@ -103,7 +123,7 @@ export const ProjectSettings: React.FC = () => {
content: t('projects.edit.update_members_success'),
});
})
.catch((error) => {
.catch((error: any) => {
pushNotification({
type: 'error',
content: t('common.server_error', { error: error?.data?.detail?.msg }),
Expand All @@ -113,14 +133,38 @@ export const ProjectSettings: React.FC = () => {

const debouncedMembersHandler = useCallback(debounce(changeMembersHandler, 1000), []);

const changeVisibilityHandler = (is_public: boolean) => {
updateProject({
project_name: paramProjectName,
is_public: is_public,
})
.unwrap()
.then(() => {
pushNotification({
type: 'success',
content: t('projects.edit.update_visibility_success'),
});
})
.catch((error: any) => {
pushNotification({
type: 'error',
content: t('common.server_error', { error: error?.data?.detail?.msg }),
});
});
};

const isDisabledButtons = useMemo<boolean>(() => {
return isDeleting || !data || !isAvailableDeletingPermission(data);
}, [data, isDeleting, isAvailableDeletingPermission]);

const deleteProjectHandler = () => {
if (!data) return;

deleteProject(data).then(() => navigate(ROUTES.PROJECT.LIST));
deleteProject(data)
.then(() => navigate(ROUTES.PROJECT.LIST))
.catch((error: any) => {
console.error('Delete project failed:', error);
});
};

if (isLoadingPage)
Expand All @@ -134,42 +178,44 @@ export const ProjectSettings: React.FC = () => {
<>
{data && backendsData && gatewaysData && (
<SpaceBetween size="l">
<Container
header={
<Header variant="h2" info={<InfoLink onFollow={() => openHelpPanel(CLI_INFO)} />}>
{t('projects.edit.cli')}
</Header>
}
>
<SpaceBetween size="s">
<Box variant="p" color="text-body-secondary">
Run the following commands to set up the CLI for this project
</Box>

<div className={styles.codeWrapper}>
<Hotspot hotspotId={HotspotIds.CONFIGURE_CLI_COMMAND}>
<Code className={styles.code}>{configCliCommand}</Code>

<div className={styles.copy}>
<Popover
dismissButton={false}
position="top"
size="small"
triggerType="custom"
content={<StatusIndicator type="success">{t('common.copied')}</StatusIndicator>}
>
<Button
formAction="none"
iconName="copy"
variant="normal"
onClick={copyCliCommand}
/>
</Popover>
</div>
</Hotspot>
</div>
</SpaceBetween>
</Container>
{isProjectMember && (
<Container
header={
<Header variant="h2" info={<InfoLink onFollow={() => openHelpPanel(CLI_INFO)} />}>
{t('projects.edit.cli')}
</Header>
}
>
<SpaceBetween size="s">
<Box variant="p" color="text-body-secondary">
Run the following commands to set up the CLI for this project
</Box>

<div className={styles.codeWrapper}>
<Hotspot hotspotId={HotspotIds.CONFIGURE_CLI_COMMAND}>
<Code className={styles.code}>{configCliCommand}</Code>

<div className={styles.copy}>
<Popover
dismissButton={false}
position="top"
size="small"
triggerType="custom"
content={<StatusIndicator type="success">{t('common.copied')}</StatusIndicator>}
>
<Button
formAction="none"
iconName="copy"
variant="normal"
onClick={copyCliCommand}
/>
</Popover>
</div>
</Hotspot>
</div>
</SpaceBetween>
</Container>
)}

<BackendsTable
backends={backendsData}
Expand All @@ -190,6 +236,7 @@ export const ProjectSettings: React.FC = () => {
members={data.members}
readonly={!isProjectManager(data)}
isAdmin={isProjectAdmin(data)}
project={data}
/>

<Container header={<Header variant="h2">{t('common.danger_zone')}</Header>}>
Expand All @@ -216,6 +263,43 @@ export const ProjectSettings: React.FC = () => {
</>
)}

{isAvailableProjectManaging && (
<>
<Box variant="h5" color="text-body-secondary">
{t('projects.edit.project_visibility')}
</Box>

<div>
<ButtonWithConfirmation
variant="danger-normal"
disabled={!isProjectManager(data)}
formAction="none"
onClick={() => changeVisibilityHandler(selectedVisibility.value === 'public')}
confirmTitle={t('projects.edit.update_visibility_confirm_title')}
confirmButtonLabel={t('projects.edit.change_visibility')}
confirmContent={
<SpaceBetween size="s">
<Box variant="p" color="text-body-secondary">
{t('projects.edit.update_visibility_confirm_message')}
</Box>
<div className={styles.dangerSectionField}>
<SelectCSD
options={visibilityOptions}
selectedOption={selectedVisibility}
onChange={(event) => setSelectedVisibility(event.detail.selectedOption as { label: string; value: string })}
expandToViewport={true}
filteringType="auto"
/>
</div>
</SpaceBetween>
}
>
{t('projects.edit.change_visibility')}
</ButtonWithConfirmation>
</div>
</>
)}

<Box variant="h5" color="text-body-secondary">
{t('projects.edit.owner')}
</Box>
Expand Down
31 changes: 27 additions & 4 deletions frontend/src/pages/Project/Details/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import React from 'react';
import { Outlet, useParams } from 'react-router-dom';
import React, { useMemo } from 'react';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

import { ContentLayout, DetailsHeader } from 'components';
import { Button, ContentLayout, DetailsHeader } from 'components';

import { useAppSelector, useNotifications } from 'hooks';
import { selectUserData } from 'App/slice';
import { ROUTES } from 'routes';
import { useGetProjectQuery, useAddProjectMemberMutation, useRemoveProjectMemberMutation } from 'services/project';
import { getProjectRoleByUserName } from '../utils';
import { useProjectMemberActions } from '../hooks/useProjectMemberActions';

export const ProjectDetails: React.FC = () => {
const { t } = useTranslation();
const params = useParams();
const navigate = useNavigate();
const paramProjectName = params.projectName ?? '';
const userData = useAppSelector(selectUserData);
const { handleJoinProject, handleLeaveProject, isMemberActionLoading } = useProjectMemberActions();

const { data: project } = useGetProjectQuery({ name: paramProjectName });

const currentUserRole = useMemo(() => {
if (!userData?.username || !project) return null;
return getProjectRoleByUserName(project, userData.username);
}, [project, userData?.username]);

const isProjectOwner = userData?.username === project?.owner.username;

const isMember = currentUserRole !== null;

return (
<ContentLayout header={<DetailsHeader title={paramProjectName} />}>
<ContentLayout header={<DetailsHeader title={paramProjectName}/>}>
<Outlet />
</ContentLayout>
);
Expand Down
Loading