Skip to content

Commit

Permalink
feat(ui): add more detail + filter to project list page (#2201)
Browse files Browse the repository at this point in the history
Signed-off-by: Remington Breeze <[email protected]>
  • Loading branch information
rbreeze authored Jul 2, 2024
1 parent 132b288 commit a5887d9
Show file tree
Hide file tree
Showing 12 changed files with 1,137 additions and 832 deletions.
12 changes: 6 additions & 6 deletions api/rbac/v1alpha1/role_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ type RoleResources struct {
}

type ResourceDetails struct {
ResourceType string `json:"resourceType,omitempty"`
ResourceName string `json:"resourceName,omitempty"`
Verbs []string `json:"verbs,omitempty"`
ResourceType string `json:"resourceType,omitempty" protobuf:"bytes,1,opt,name=resourceType"`
ResourceName string `json:"resourceName,omitempty" protobuf:"bytes,2,opt,name=resourceName"`
Verbs []string `json:"verbs,omitempty" protobuf:"bytes,3,rep,name=verbs"`
}

type UserClaims struct {
Subs []string `json:"subs,omitempty"`
Emails []string `json:"emails,omitempty"`
Groups []string `json:"groups,omitempty"`
Subs []string `json:"subs,omitempty" protobuf:"bytes,1,rep,name=subs"`
Emails []string `json:"emails,omitempty" protobuf:"bytes,2,rep,name=emails"`
Groups []string `json:"groups,omitempty" protobuf:"bytes,3,rep,name=groups"`
}
5 changes: 4 additions & 1 deletion api/service/v1alpha1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,14 @@ message GetProjectResponse {
}

message ListProjectsRequest {
/* explicitly empty */
optional int32 page_size = 1;
optional int32 page = 2;
optional string filter = 3;
}

message ListProjectsResponse {
repeated github.com.akuity.kargo.api.v1alpha1.Project projects = 1;
int32 total = 2;
}

message ApproveFreightRequest {
Expand Down
39 changes: 35 additions & 4 deletions internal/api/list_projects_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package api
import (
"context"
"fmt"
"sort"
"slices"
"strings"

"connectrpc.com/connect"

Expand All @@ -13,22 +14,52 @@ import (

func (s *server) ListProjects(
ctx context.Context,
_ *connect.Request[svcv1alpha1.ListProjectsRequest],
req *connect.Request[svcv1alpha1.ListProjectsRequest],
) (*connect.Response[svcv1alpha1.ListProjectsResponse], error) {
var list kargoapi.ProjectList
if err := s.client.List(ctx, &list); err != nil {
return nil, fmt.Errorf("error listing Projects: %w", err)
}

sort.Slice(list.Items, func(i, j int) bool {
return list.Items[i].Name < list.Items[j].Name
slices.SortFunc(list.Items, func(a, b kargoapi.Project) int {
return strings.Compare(a.Name, b.Name)
})

var filtered []kargoapi.Project
if req.Msg.GetFilter() != "" {
filter := strings.ToLower(req.Msg.GetFilter())
for i := 0; i < len(list.Items); i++ {
if strings.Contains(strings.ToLower(list.Items[i].Name), filter) {
filtered = append(filtered, list.Items[i])
}
}
list.Items = filtered
}

total := len(list.Items)
pageSize := len(list.Items)
if req.Msg.GetPageSize() > 0 {
pageSize = int(req.Msg.GetPageSize())
}

start := int(req.Msg.GetPage()) * pageSize
end := start + pageSize

if start >= len(list.Items) {
return connect.NewResponse(&svcv1alpha1.ListProjectsResponse{}), nil
}

if end > len(list.Items) {
end = len(list.Items)
}

list.Items = list.Items[start:end]
projects := make([]*kargoapi.Project, len(list.Items))
for i := range list.Items {
projects[i] = &list.Items[i]
}
return connect.NewResponse(&svcv1alpha1.ListProjectsResponse{
Projects: projects,
Total: int32(total),
}), nil
}
4 changes: 3 additions & 1 deletion internal/api/list_projects_v1alpha1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ func TestListProjects(t *testing.T) {
svr := &server{
client: client,
}
res, err := (svr).ListProjects(ctx, nil)
res, err := (svr).ListProjects(ctx, &connect.Request[svcv1alpha1.ListProjectsRequest]{
Msg: &svcv1alpha1.ListProjectsRequest{},
})
testCase.assertions(t, res, err)
})
}
Expand Down
6 changes: 6 additions & 0 deletions internal/api/list_stages_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package api
import (
"context"
"fmt"
"slices"
"strings"

"connectrpc.com/connect"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -29,6 +31,10 @@ func (s *server) ListStages(
return nil, fmt.Errorf("list stages: %w", err)
}

slices.SortFunc(list.Items, func(a, b kargoapi.Stage) int {
return strings.Compare(a.Name, b.Name)
})

stages := make([]*kargoapi.Stage, len(list.Items))
for idx := range list.Items {
stages[idx] = &list.Items[idx]
Expand Down
1,643 changes: 843 additions & 800 deletions pkg/api/service/v1alpha1/service.pb.go

Large diffs are not rendered by default.

70 changes: 62 additions & 8 deletions ui/src/features/project/list/project-item/project-item.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
import { Tooltip } from 'antd';
import classNames from 'classnames';
import { Link, generatePath } from 'react-router-dom';

import { paths } from '@ui/config/paths';
import { Description } from '@ui/features/common/description';
import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon';
import { PromotionStatusIcon } from '@ui/features/common/promotion-status/promotion-status-icon';
import { getStageColors } from '@ui/features/stage/utils';
import { Project, Stage } from '@ui/gen/v1alpha1/generated_pb';

import * as styles from './project-item.module.less';
import { StagePopover } from './stage-popover';

type Props = {
name: string;
};
export const ProjectItem = ({ project, stages }: { project?: Project; stages?: Stage[] }) => {
const stageColorMap = getStageColors(project?.metadata?.name || '', stages || []);

export const ProjectItem = ({ name }: Props) => (
<Link className={styles.tile} to={generatePath(paths.project, { name })}>
<div className={styles.title}>{name}</div>
</Link>
);
return (
<Link
className={styles.tile}
to={generatePath(paths.project, { name: project?.metadata?.name })}
>
<div className={classNames(styles.title, 'mb-2')}>{project?.metadata?.name}</div>
<Description item={project as Project} loading={false} />
{(stages || []).length > 0 && (
<div className='flex items-center gap-x-3 gap-y-1 flex-wrap mt-4'>
{stages?.map((stage) => (
<Tooltip
key={stage.metadata?.name}
placement='bottom'
title={
stage?.status?.lastPromotion?.name && (
<StagePopover
promotionName={stage?.status?.lastPromotion?.name}
project={project?.metadata?.name}
freightName={stage?.status?.currentFreight?.name}
stageName={stage?.metadata?.name}
/>
)
}
>
<div
className='flex items-center mb-2 text-white rounded py-1 px-2 font-semibold bg-gray-600'
style={{ backgroundColor: stageColorMap[stage.metadata?.name || ''] }}
>
{stage.status?.health && (
<div className='mr-2'>
<HealthStatusIcon health={stage.status?.health} hideColor={true} />
</div>
)}
{!stage?.status?.currentPromotion && stage.status?.lastPromotion && (
<div className='mr-2'>
<PromotionStatusIcon
placement='top'
status={stage.status?.lastPromotion?.status}
color='white'
size='1x'
/>
</div>
)}
{stage.metadata?.name}
</div>
</Tooltip>
))}
</div>
)}
</Link>
);
};
66 changes: 66 additions & 0 deletions ui/src/features/project/list/project-item/stage-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useQuery } from '@connectrpc/connect-query';
import { faBox, faClock } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import moment from 'moment';
import { useMemo } from 'react';
import { generatePath, useNavigate } from 'react-router-dom';

import { paths } from '@ui/config/paths';
import { getAlias } from '@ui/features/common/utils';
import {
getFreight,
getPromotion
} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { Freight, Promotion } from '@ui/gen/v1alpha1/generated_pb';

export const StagePopover = ({
promotionName,
project,
freightName,
stageName
}: {
promotionName: string;
project?: string;
freightName?: string;
stageName?: string;
}) => {
const { data: promotionData } = useQuery(getPromotion, { name: promotionName, project });
const promotion = useMemo(() => promotionData?.result?.value as Promotion, [promotionData]);
const { data: freightData } = useQuery(
getFreight,
{ name: freightName, project },
{ enabled: !!freightName }
);

const _label = ({ children }: { children: string }) => (
<div className='text-xs font-semibold text-neutral-300 mb-1'>{children}</div>
);

const navigate = useNavigate();

return (
<div>
<_label>LAST PROMOTED</_label>
<div className='flex items-center mb-4'>
<FontAwesomeIcon icon={faClock} className='mr-2' />
<div>
{moment(promotion?.metadata?.creationTimestamp?.toDate()).format('MMM do yyyy HH:mm:ss')}
</div>
</div>
<_label>CURRENT FREIGHT</_label>
<div className='flex items-center mb-2'>
<FontAwesomeIcon icon={faBox} className='mr-2' />
<div>{getAlias(freightData?.result?.value as Freight)}</div>
</div>
<div
onClick={(e) => {
e.preventDefault();
navigate(generatePath(paths.stage, { name: project, stageName }));
}}
className='underline text-blue-400 font-semibold w-full text-center cursor-pointer'
>
DETAILS
</div>
</div>
);
};
38 changes: 38 additions & 0 deletions ui/src/features/project/list/project-list-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useQuery } from '@connectrpc/connect-query';
import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AutoComplete, Button } from 'antd';
import { useState } from 'react';

import { listProjects } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';

export const ProjectListFilter = ({
onChange,
init
}: {
onChange: (filter: string) => void;
init?: string;
}) => {
const { data } = useQuery(listProjects);
const [filter, setFilter] = useState(init || '');

return (
<div className='flex items-center w-2/3'>
<AutoComplete
placeholder='Filter...'
options={data?.projects.map((p) => ({ value: p.metadata?.name }))}
onChange={setFilter}
className='w-full mr-2'
value={filter}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onChange(filter);
}
}}
/>
<Button type='primary' onClick={() => onChange(filter)}>
<FontAwesomeIcon icon={faMagnifyingGlass} />
</Button>
</div>
);
};
2 changes: 1 addition & 1 deletion ui/src/features/project/list/projects-list.module.less
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.list {
display: grid;
margin-top: ~'@{sizeMD}px';
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: ~'@{sizeMD}px';
}
58 changes: 49 additions & 9 deletions ui/src/features/project/list/projects-list.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
import { useQuery } from '@connectrpc/connect-query';
import { Empty } from 'antd';
import { createQueryOptions, useQuery, useTransport } from '@connectrpc/connect-query';
import { useQueries } from '@tanstack/react-query';
import { Empty, Pagination } from 'antd';
import { useState } from 'react';

import { LoadingState } from '@ui/features/common';
import { listProjects } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import {
listProjects,
listStages
} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';

import { ProjectItem } from './project-item/project-item';
import { ProjectListFilter } from './project-list-filter';
import * as styles from './projects-list.module.less';

export const ProjectsList = () => {
const { data, isLoading } = useQuery(listProjects, {});
const [pageSize, setPageSize] = useState(9);
const [page, setPage] = useState(1);
const [filter, setFilter] = useState('');

const { data, isLoading } = useQuery(listProjects, {
pageSize: pageSize,
page: page - 1,
filter
});

const transport = useTransport();
const stageData = useQueries({
queries: (data?.projects || []).map((proj) => {
return createQueryOptions(listStages, { project: proj?.metadata?.name }, { transport });
})
});

if (isLoading) return <LoadingState />;

if (!data || data.projects.length === 0) return <Empty />;

return (
<div className={styles.list}>
{data.projects.map((project) => (
<ProjectItem key={project.metadata?.name} name={project.metadata?.name} />
))}
</div>
<>
<div className='flex items-center mb-6'>
<ProjectListFilter onChange={setFilter} init={filter} />
<Pagination
total={data?.total || 0}
className='ml-auto flex-shrink-0'
pageSize={pageSize}
current={page}
onChange={(page, pageSize) => {
setPage(page);
setPageSize(pageSize);
}}
/>
</div>
<div className={styles.list}>
{data.projects.map((proj, i) => (
<ProjectItem
key={proj?.metadata?.name}
project={proj}
stages={stageData[i]?.data?.stages}
/>
))}
</div>
</>
);
};
Loading

0 comments on commit a5887d9

Please sign in to comment.