Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Delete Project button to Danger zone tab #1059

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 4 additions & 81 deletions webapp/src/dogma/common/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, useEffect, useRef, useState } from 'react';
import { ReactNode } from 'react';
import {
Box,
Button,
Flex,
HStack,
IconButton,
Kbd,
Link,
Menu,
MenuButton,
Expand All @@ -36,14 +35,13 @@ import { CloseIcon, HamburgerIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import { default as RouteLink } from 'next/link';
import { logout } from 'dogma/features/auth/authSlice';
import Router from 'next/router';
import { useGetProjectsQuery, useGetTitleQuery } from 'dogma/features/api/apiSlice';
import { ProjectDto } from 'dogma/features/project/ProjectDto';
import { components, DropdownIndicatorProps, GroupBase, OptionBase, Select } from 'chakra-react-select';
import { useGetTitleQuery } from 'dogma/features/api/apiSlice';
import { NewProject } from 'dogma/features/project/NewProject';
import { usePathname } from 'next/navigation';
import { useAppDispatch, useAppSelector } from 'dogma/hooks';
import { LabelledIcon } from 'dogma/common/components/LabelledIcon';
import { FaUser } from 'react-icons/fa';
import ProjectSearchBox from 'dogma/common/components/ProjectSearchBox';

interface TopMenu {
name: string;
Expand All @@ -66,67 +64,11 @@ const NavLink = ({ link, children }: { link: string; children: ReactNode }) => (
</Link>
);

export interface ProjectOptionType extends OptionBase {
value: string;
label: string;
}

const initialState: ProjectOptionType = {
value: '',
label: '',
};

const DropdownIndicator = (
props: JSX.IntrinsicAttributes & DropdownIndicatorProps<unknown, boolean, GroupBase<unknown>>,
) => {
return (
<components.DropdownIndicator {...props}>
<Kbd>/</Kbd>
</components.DropdownIndicator>
);
};

export const Navbar = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { colorMode, toggleColorMode } = useColorMode();
const { user } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const result = useGetProjectsQuery({ admin: false });
const projects = result.data || [];
const projectOptions: ProjectOptionType[] = projects.map((project: ProjectDto) => ({
value: project.name,
label: project.name,
}));
const [selectedOption, setSelectedOption] = useState(initialState);
const handleChange = (option: ProjectOptionType) => {
setSelectedOption(option);
};

useEffect(() => {
if (selectedOption?.value) {
Router.push(`/app/projects/${selectedOption.value}`);
}
}, [selectedOption?.value]);

const selectRef = useRef(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = (e.target as HTMLElement).tagName.toLowerCase();
if (target == 'textarea' || target == 'input') {
return;
}
if (e.key === '/') {
e.preventDefault();
selectRef.current.clearValue();
selectRef.current.focus();
} else if (e.key === 'Escape') {
selectRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);

const pathname = usePathname();

const { data: titleDto } = useGetTitleQuery();
Expand Down Expand Up @@ -159,26 +101,7 @@ export const Navbar = () => {
<div />
) : (
<Box w="40%">
<Select
id="color-select"
name="project-search"
options={projectOptions}
value={selectedOption?.value}
onChange={(option: ProjectOptionType) => option && handleChange(option)}
placeholder="Jump to project ..."
closeMenuOnSelect={true}
openMenuOnFocus={true}
isClearable={true}
isSearchable={true}
ref={selectRef}
components={{ DropdownIndicator }}
chakraStyles={{
control: (baseStyles) => ({
...baseStyles,
backgroundColor: colorMode === 'light' ? 'white' : 'whiteAlpha.50',
}),
}}
/>
<ProjectSearchBox id="nav-search" placeholder="Jump to project ..." />
</Box>
)}
<Flex alignItems="center" gap={2}>
Expand Down
111 changes: 111 additions & 0 deletions webapp/src/dogma/common/components/ProjectSearchBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
components,
DropdownIndicatorProps,
GroupBase,
OptionBase,
Select,
SizeProp,
} from 'chakra-react-select';
import { useEffect, useRef, useState } from 'react';
import { Kbd, useColorMode } from '@chakra-ui/react';
import Router from 'next/router';
import { useGetProjectsQuery } from 'dogma/features/api/apiSlice';
import { ProjectDto } from 'dogma/features/project/ProjectDto';

export interface ProjectSearchBoxProps {
id: string;
size?: SizeProp;
placeholder: string;
autoFocus?: boolean;
}

export interface ProjectOptionType extends OptionBase {
value: string;
label: string;
}

const initialState: ProjectOptionType = {
value: '',
label: '',
};

const DropdownIndicator = (
props: JSX.IntrinsicAttributes & DropdownIndicatorProps<unknown, boolean, GroupBase<unknown>>,
) => {
return (
<components.DropdownIndicator {...props}>
<Kbd>/</Kbd>
</components.DropdownIndicator>
);
};

const ProjectSearchBox = ({ id, size, placeholder, autoFocus }: ProjectSearchBoxProps) => {
const { colorMode } = useColorMode();
const { data, isLoading } = useGetProjectsQuery({ admin: false });
const projects = data || [];
const projectOptions: ProjectOptionType[] = projects.map((project: ProjectDto) => ({
value: project.name,
label: project.name,
}));

const [selectedOption, setSelectedOption] = useState(initialState);
const handleChange = (option: ProjectOptionType) => {
setSelectedOption(option);
};

const selectRef = useRef(null);
useEffect(() => {
if (selectedOption?.value) {
selectRef.current.blur();
Router.push(`/app/projects/${selectedOption.value}`);
}
}, [selectedOption?.value]);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = (e.target as HTMLElement).tagName.toLowerCase();
if (target == 'textarea' || target == 'input') {
return;
}
if (e.key === '/') {
e.preventDefault();
selectRef.current.clearValue();
selectRef.current.focus();
} else if (e.key === 'Escape') {
selectRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectRef]);

return (
<Select
size={size}
id={id}
autoFocus={autoFocus}
name="project-search"
options={projectOptions}
value={selectedOption?.value}
onChange={(option: ProjectOptionType) => option && handleChange(option)}
placeholder={placeholder}
closeMenuOnSelect={true}
openMenuOnFocus={!autoFocus}
isClearable={true}
isSearchable={true}
ref={selectRef}
isLoading={isLoading}
components={{ DropdownIndicator }}
chakraStyles={{
control: (baseStyles) => ({
...baseStyles,
backgroundColor: colorMode === 'light' ? 'white' : 'whiteAlpha.50',
}),
}}
/>
);
};

export default ProjectSearchBox;
2 changes: 1 addition & 1 deletion webapp/src/dogma/features/project/DeleteProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const DeleteProject = ({ projectName }: { projectName: string }) => {
return (
<>
<Box>
<Button colorScheme="red" variant="outline" size="sm" onClick={onToggle}>
<Button colorScheme="red" variant="outline" size="lg" onClick={onToggle}>
Delete Project
</Button>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ interface ProjectSettingsViewProps {
children: (meta: ProjectMetadataDto) => ReactNode;
}

type TabName = 'repositories' | 'permissions' | 'members' | 'tokens' | 'mirrors' | 'credentials';
type TabName =
| 'repositories'
| 'permissions'
| 'members'
| 'tokens'
| 'mirrors'
| 'credentials'
| 'danger zone';
type UserRole = 'OWNER' | 'MEMBER' | 'GUEST';

export interface TapInfo {
Expand All @@ -52,6 +59,7 @@ const TABS: TapInfo[] = [
{ name: 'tokens', path: 'tokens', accessRole: 'OWNER', allowAnonymous: false },
{ name: 'mirrors', path: 'mirrors', accessRole: 'OWNER', allowAnonymous: true },
{ name: 'credentials', path: 'credentials', accessRole: 'OWNER', allowAnonymous: true },
{ name: 'danger zone', path: 'danger-zone', accessRole: 'OWNER', allowAnonymous: true },
];

function isAllowed(userRole: string, anonymous: boolean, tabInfo: TapInfo): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { newRandomCredential } from 'pages/api/v1/projects/[projectName]/credentials/index';
import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto';

const credentials: Map<number, CredentialDto> = new Map();
const credentials: Map<string, CredentialDto> = new Map();

export default function handler(req: NextApiRequest, res: NextApiResponse) {
const index = parseInt(req.query.index as string, 10);
const id = req.query.id as string;
switch (req.method) {
case 'GET':
const credential = newRandomCredential(index);
credentials.set(index, credential);
const credential = newRandomCredential(id);
res.status(200).json(credential);
break;
case 'PUT':
credentials.set(index, req.body);
credentials.set(id, req.body);
res.status(201).json(`${credentials.size + 2}`);
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@
import { NextApiRequest, NextApiResponse } from 'next';
import _ from 'lodash';
import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto';
import { faker } from '@faker-js/faker';

const credentials: CredentialDto[] = _.range(0, 20).map((i) => newRandomCredential(i));
const credentials: CredentialDto[] = _.range(0, 20).map(() =>
newRandomCredential('credential-' + faker.random.word()),
);

export function newRandomCredential(index: number): CredentialDto {
export function newRandomCredential(id: string): CredentialDto {
const index = id.length;
switch (index % 4) {
case 0:
return {
id: `password-id-${index}`,
id,
type: 'password',
username: `username-${index}`,
password: `password-${index}`,
enabled: true,
};
case 1:
return {
id: `public-key-id-${index}`,
id,
type: 'public_key',
username: `username-${index}`,
publicKey: `public-key-${index}`,
Expand All @@ -42,14 +46,14 @@ export function newRandomCredential(index: number): CredentialDto {
};
case 2:
return {
id: `access-token-id-${index}`,
id,
type: 'access_token',
accessToken: `access-token-${index}`,
enabled: true,
};
case 3:
return {
id: `none-id-${index}`,
id,
type: 'none',
enabled: true,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { FileDto, FileType } from 'dogma/features/file/FileDto';
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';

const newFile = (id: number): FileDto => {
const type: FileType = faker.helpers.arrayElement(['TEXT', 'DIRECTORY', 'JSON', 'YML']);
const extension = type == 'DIRECTORY' ? '' : '.' + type.toLowerCase();
return {
revision: faker.datatype.number({
min: 1,
max: 10,
}),
path: `/${id}-${faker.animal.rabbit().replaceAll(' ', '-').toLowerCase()}${extension}`,
type: type,
url: faker.internet.url(),
};
};
const fileList: FileDto[] = [];
const makeData = (len: number) => {
for (let i = 0; i < len; i++) {
fileList.push(newFile(i));
}
};
makeData(20);

export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { fileItem } = req.body;
const { query } = req;
const { revision } = query;
const revisionNumber = parseInt(revision as string);
const filtered = isNaN(revisionNumber)
? fileList
: fileList.filter((file: FileDto) => file.revision <= revisionNumber);
switch (req.method) {
case 'GET':
res.status(200).json(filtered);
break;
case 'POST':
fileList.push(fileItem);
res.status(200).json(fileList);
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
break;
}
}
Loading
Loading