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

feat: Basic StepDrawer components implementation #1021

Merged
merged 8 commits into from
Jun 24, 2024
Merged
Next Next commit
feat: implement StepDrawer components
  • Loading branch information
zoltanszabo-bitrise committed Jun 21, 2024
commit ae497645631d6ec13a6df8e5eab35b85257ebcc7
108 changes: 108 additions & 0 deletions source/javascripts/components/StepDrawer/StepDrawer.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Step } from './StepDrawer.types';

export const MockSteps: Step[] = [
{
id: 'activate-ssh-key',
title: 'Activate SSH key',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/activate-ssh-key/assets/icon.svg',
version: '1.1.6',
description:
'Add your SSH key to the build machine to access private repositories\n' +
'\n' +
'This Step makes sure Bitrise has access to your repository when cloning SSH URLs. The Step saves the provided private key of your SSH keypair to a file and then loads it into the SSH agent.',
categories: ['access-control'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'clone',
title: 'Git Clone',
description:
'Checks out the repository, updates submodules and exports git metadata as Step outputs.\n' +
'\n' +
'The checkout process depends on the Step settings and the build trigger parameters (coming from your git server).',
version: '8.3.1',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/git-clone/assets/icon.svg',
categories: ['utility'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'npm',
title: 'Run npm command',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/npm/assets/icon.svg',
version: '1.1.6',
description:
"The Step runs npm with the command and arguments you provide, for example, to install missing packages or run a package's test.",
categories: ['build', 'test', 'utility'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'test',
title: 'Xcode Test for iOS',
description: "Runs your project's pre-defined Xcode tests on every build.",
version: '5.1.1',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/xcode-test/assets/icon.svg',
categories: ['test'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'xcode-archive',
title: 'Xcode Archive & Export for iOS',
description:
'Archive and export an Xcode project.\n' +
'\n' +
'This Step will archive your Xcode project and export it as an .ipa file. You can also export the archive as a .xcarchive file.',
version: '5.1.2',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/xcode-archive/assets/icon.svg',
categories: ['test', 'build', 'deploy'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'codecov',
title: 'Codecov',
description:
'Upload your code coverage files to Codecov.io\n' +
'\n' +
'Reduce technical debt with visualized test performance, faster code reviews and workflow enhancements. Now you can upload your code coverage files to Codecov every time you kick off a build!',
version: '3.3.3',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/codecov/assets/icon.svg',
categories: ['test'],
isOfficial: false,
isVerified: true,
isDeprecated: false,
},
{
id: 'deploy-to-bitrise-io',
title: 'Deploy to Bitrise.io',
description:
"Deploys build artifacts to make them available for the user on the build's Artifacts tab.\n" +
"Sends test results to the Test Reports add-on (build's Tests tab). Uploads Pipeline intermediate files to make them available in subsequent Stages and also uploads Bitrise and user generated html reports.",
version: '2.8.1',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/deploy-to-bitrise-io/assets/icon.svg',
categories: ['test', 'deploy'],
isOfficial: true,
isVerified: false,
isDeprecated: false,
},
{
id: 'azure-devops-status',
title: 'Azure DevOps Status',
description:
'Update commit status for Azure DevOps repositories. This step always runs, no matter if build succeeded or failed.',
version: '1.0.1',
icon: 'https://bitrise-steplib-collection.s3.amazonaws.com/steps/azure-devops-status/assets/icon.svg',
categories: ['test'],
isOfficial: false,
isVerified: false,
isDeprecated: true,
},
];
14 changes: 14 additions & 0 deletions source/javascripts/components/StepDrawer/StepDrawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Meta, StoryObj } from '@storybook/react';
import StepDrawer from './StepDrawer';

export default {
component: StepDrawer,
argTypes: {
isOpen: { control: 'boolean' },
},
args: {
isOpen: true,
},
} as Meta<typeof StepDrawer>;

export const Default: StoryObj = {};
37 changes: 37 additions & 0 deletions source/javascripts/components/StepDrawer/StepDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Box, Drawer, Text, useDisclosure } from '@bitrise/bitkit';
import { FormProvider, useForm } from 'react-hook-form';
import { FilterFormValues } from './StepDrawer.types';
import StepFilter from './components/StepFilter';
import StepList from './components/StepList';
import { useCategories, useFilter } from './hooks/useSteps';

type Props = {
isOpen: boolean;
};

const StepDrawer = ({ isOpen: isInitialOpen }: Props) => {
const { isOpen, onClose } = useDisclosure({ defaultIsOpen: isInitialOpen });
const { categories } = useCategories();
const form = useForm<FilterFormValues>({
defaultValues: {
search: '',
categories: [],
},
});

const { steps } = useFilter(form.watch());
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved

return (
<FormProvider {...form}>
<Drawer maxWidth={['100%', '50%']} title="Steps" isOpen={isOpen} onClose={onClose}>
<Box display="flex" gap="16" flexDir="column">
<Text textStyle="heading/h3">Add Step</Text>
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved
<StepFilter />
<StepList categories={categories} steps={steps} />
</Box>
</Drawer>
</FormProvider>
);
};

export default StepDrawer;
17 changes: 17 additions & 0 deletions source/javascripts/components/StepDrawer/StepDrawer.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type FilterFormValues = {
search: string;
projectType: string;
categories: string[];
};

export type Step = {
AndrasEszes marked this conversation as resolved.
Show resolved Hide resolved
id: string;
icon: string;
title: string;
description: string;
version: string;
categories: string[];
isOfficial: boolean;
isVerified: boolean;
isDeprecated: boolean;
};
3 changes: 3 additions & 0 deletions source/javascripts/components/StepDrawer/StepDrawer.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import capitalize from 'lodash/capitalize';

export const displayCategoryName = (category: string) => capitalize(category).replace('-', ' ');
57 changes: 57 additions & 0 deletions source/javascripts/components/StepDrawer/components/StepCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Box, Card, Icon, Text } from '@bitrise/bitkit';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { useBoolean } from 'usehooks-ts';
import { Step } from '../StepDrawer.types';
import defaultStepIcon from '../../../../images/step/icon-default.svg';
import StepBadge from '../../StepBadge/StepBadge';

type Props = Step;

const StepCard = ({ icon, title, description, version, isOfficial, isVerified, isDeprecated }: Props) => {
const { value: hovered, setTrue, setFalse } = useBoolean(false);
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved

return (
<Card
variant="outline"
position="relative"
minW="256px"
padding="12"
onMouseEnter={setTrue}
onMouseLeave={setFalse}
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved
>
<Box display="flex" gap="8">
<Box position="relative">
<LazyLoadImage
effect="blur"
src={icon || defaultStepIcon}
style={{
height: '40px',
borderRadius: '4px',
border: '1px solid var(--colors-neutral-90)',
}}
/>
<StepBadge
position="absolute"
top="21px"
left="21px"
isOfficial={isOfficial}
isVerified={isVerified}
isDeprecated={isDeprecated}
/>
</Box>
<Box>
<Text textStyle="body/lg/semibold">{title}</Text>
<Text textStyle="body/md/regular" color="text/secondary">
{version}
</Text>
</Box>
</Box>
<Text textStyle="body/sm/regular" color="text/secondary" hasEllipsis>
{description}
</Text>
{hovered && <Icon position="absolute" top={12} right={12} name="PlusAdd" color="icon/interactive" />}
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved
</Card>
);
};

export default StepCard;
31 changes: 31 additions & 0 deletions source/javascripts/components/StepDrawer/components/StepFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, SearchInput, Tag } from '@bitrise/bitkit';
import { Controller } from 'react-hook-form';

type Props = {
categories?: string[];
};

const StepFilter = ({ categories = [] }: Props) => {
return (
<Box>
<Controller
name="search"
render={({ field: { ref, onChange, ...rest } }) => (
<SearchInput
inputRef={ref}
placeholder="Filter by name or description"
onChange={(value) => onChange({ target: { value } })}
{...rest}
/>
)}
/>
<Box display="flex" flexWrap="wrap" gap="16">
{categories.map((category) => (
<Tag>{category}</Tag>
))}
</Box>
</Box>
);
};

export default StepFilter;
46 changes: 46 additions & 0 deletions source/javascripts/components/StepDrawer/components/StepList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useMemo } from 'react';
import { Box, Text } from '@bitrise/bitkit';
import { SimpleGrid } from '@chakra-ui/react';
import { Step } from '../StepDrawer.types';
import { displayCategoryName } from '../StepDrawer.utils';
import StepCard from './StepCard';

type Props = {
categories: string[];
steps: Step[];
};

const StepList = ({ categories = [], steps = [] }: Props) => {
const stepByCategories = useMemo(
() =>
steps.reduce(
(acc, step) => {
step.categories.forEach((category) => {
acc[category] ||= [];
acc[category].push(step);
});

return acc;
},
{} as Record<string, Step[]>,
),
[steps],
);

return (
<Box display="flex" flexDir="column" gap="16">
{categories?.map((category) => (
<Box key={category}>
<Text textStyle="heading/h4" marginBottom="8">
{displayCategoryName(category)}
</Text>
<SimpleGrid columns={[1, 2]} spacing="16">
{stepByCategories[category]?.map((step) => <StepCard key={step.id} {...step} />)}
</SimpleGrid>
</Box>
))}
</Box>
);
};

export default StepList;
37 changes: 37 additions & 0 deletions source/javascripts/components/StepDrawer/hooks/useSteps.ts
zoltanszabo-bitrise marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMemo } from 'react';
import uniq from 'lodash/uniq';
import { MockSteps } from '../StepDrawer.mocks';
import { FilterFormValues } from '../StepDrawer.types';

export const useSteps = () => {
const steps = useMemo(() => MockSteps, []);
return { steps };
};

export const useCategories = () => {
const { steps } = useSteps();
const categories = useMemo(() => {
return uniq(steps.flatMap((step) => step.categories));
}, [steps]);
return { categories };
};

export const useFilter = ({ categories, search }: FilterFormValues) => {
const { steps } = useSteps();
const searchValue = search.toLowerCase();
let filteredSteps = steps;

if (categories.length > 0) {
filteredSteps = steps.filter((step) => step.categories.some((category) => categories.includes(category)));
}

if (searchValue) {
filteredSteps = steps.filter((step) => {
const name = step.title.toLowerCase();
const description = step.description.toLowerCase();
return name.includes(searchValue) || description.includes(searchValue);
});
}

return { steps: filteredSteps };
};