Skip to content

Commit

Permalink
feat: implement project details live refresh
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Matyushentsev <[email protected]>
  • Loading branch information
alexmt committed Aug 17, 2023
1 parent 3c5885d commit 292da98
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 316 deletions.
1 change: 1 addition & 0 deletions api/service/v1alpha1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ message WatchStageRequest {

message WatchStageResponse {
github.com.akuity.kargo.pkg.api.v1alpha1.Stage stage = 1;
string type = 2;
}

message UpdateStageRequest {
Expand Down
28 changes: 15 additions & 13 deletions internal/api/handler/watch_stage_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,27 @@ func WatchStageV1Alpha1(
if req.Msg.GetProject() == "" {
return connect.NewError(connect.CodeInvalidArgument, errors.New("project should not be empty"))
}
if req.Msg.GetName() == "" {
return connect.NewError(connect.CodeInvalidArgument, errors.New("name should not be empty"))
}
if err := validateProject(ctx, req.Msg.GetProject()); err != nil {
return err
}

if err := kubeCli.Get(ctx, client.ObjectKey{
Namespace: req.Msg.GetProject(),
Name: req.Msg.GetName(),
}, &kargov1alpha1.Stage{}); err != nil {
if kubeerr.IsNotFound(err) {
return connect.NewError(connect.CodeNotFound, err)
if req.Msg.GetName() != "" {
if err := kubeCli.Get(ctx, client.ObjectKey{
Namespace: req.Msg.GetProject(),
Name: req.Msg.GetName(),
}, &kargov1alpha1.Stage{}); err != nil {
if kubeerr.IsNotFound(err) {
return connect.NewError(connect.CodeNotFound, err)
}
return connect.NewError(connect.CodeInternal, err)

Check warning on line 53 in internal/api/handler/watch_stage_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/handler/watch_stage_v1alpha1.go#L45-L53

Added lines #L45 - L53 were not covered by tests
}
return connect.NewError(connect.CodeInternal, err)
}

w, err := stageCli.Namespace(req.Msg.GetProject()).Watch(ctx, metav1.ListOptions{
FieldSelector: fields.OneTermEqualSelector(metav1.ObjectNameField, req.Msg.GetName()).String(),
})
opts := metav1.ListOptions{}
if req.Msg.GetName() != "" {
opts.FieldSelector = fields.OneTermEqualSelector(metav1.ObjectNameField, req.Msg.GetName()).String()
}
w, err := stageCli.Namespace(req.Msg.GetProject()).Watch(ctx, opts)

Check warning on line 61 in internal/api/handler/watch_stage_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/handler/watch_stage_v1alpha1.go#L57-L61

Added lines #L57 - L61 were not covered by tests
if err != nil {
return errors.Wrap(err, "watch stage")
}
Expand All @@ -80,6 +81,7 @@ func WatchStageV1Alpha1(
}
if err := stream.Send(&svcv1alpha1.WatchStageResponse{
Stage: typesv1alpha1.ToStageProto(*stage),
Type: string(e.Type),

Check warning on line 84 in internal/api/handler/watch_stage_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/handler/watch_stage_v1alpha1.go#L84

Added line #L84 was not covered by tests
}); err != nil {
return errors.Wrap(err, "send response")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"

"connectrpc.com/connect"
grpchealth "connectrpc.com/grpchealth"
"connectrpc.com/grpchealth"
"github.com/pkg/errors"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
Expand Down
611 changes: 310 additions & 301 deletions pkg/api/service/v1alpha1/service.pb.go

Large diffs are not rendered by default.

57 changes: 56 additions & 1 deletion ui/src/features/project/project-details/project-details.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
import { FlowAnalysisGraph, FlowGraphEdgeData, IGraph, LabelStyle } from '@ant-design/graphs';
import { useQuery } from '@tanstack/react-query';
import { createPromiseClient } from '@bufbuild/connect';
import { createConnectTransport } from '@bufbuild/connect-web';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Empty } from 'antd';
import React from 'react';
import { generatePath, useNavigate, useParams } from 'react-router-dom';

import { paths } from '@ui/config/paths';
import { LoadingState } from '@ui/features/common';
import { listStages } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { KargoService } from '@ui/gen/service/v1alpha1/service_connect';
import { Stage } from '@ui/gen/v1alpha1/types_pb';
import { useDocumentEvent } from '@ui/utils/document';

export const ProjectDetails = () => {
const { name } = useParams();
const navigate = useNavigate();
const { data, isLoading } = useQuery(listStages.useQuery({ project: name }));
const graphRef = React.useRef<IGraph | undefined>();
const client = useQueryClient();

const isVisible = useDocumentEvent(
'visibilitychange',
() => document.visibilityState === 'visible'
);

React.useEffect(() => {
if (!data || !isVisible) {
return;
}

const cancel = new AbortController();

const transport = createConnectTransport({ baseUrl: '' });
const promiseClient = createPromiseClient(KargoService, transport);
const watchStages = async () => {
const stream = promiseClient.watchStage(
{ project: 'kargo-demo', name: 'test' },
{ signal: cancel.signal }
);

for await (const e of stream) {
const key = listStages.getQueryKey({ project: name });
const index = data.stages.findIndex(
(item) => item.metadata?.name === e.stage?.metadata?.name
);
let stages = data.stages;
if (e.type === 'DELETED') {
if (index !== -1) {
stages = [...stages.slice(0, index), ...data.stages.slice(index + 1)];
}
} else {
if (index === -1) {
stages = [...stages, e.stage as Stage];
} else {
stages = [
...data.stages.slice(0, index),
e.stage as Stage,
...data.stages.slice(index + 1)
];
}
}
client.setQueryData(key, { stages });
}
};
watchStages();

return cancel.abort;
}, [isLoading, isVisible]);

const nodes = React.useMemo(
() =>
Expand Down
6 changes: 6 additions & 0 deletions ui/src/gen/service/v1alpha1/service_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,11 @@ export class WatchStageResponse extends Message<WatchStageResponse> {
*/
stage?: Stage;

/**
* @generated from field: string type = 2;
*/
type = "";

constructor(data?: PartialMessage<WatchStageResponse>) {
super();
proto3.util.initPartial(data, this);
Expand All @@ -739,6 +744,7 @@ export class WatchStageResponse extends Message<WatchStageResponse> {
static readonly typeName = "akuity.io.kargo.service.v1alpha1.WatchStageResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "stage", kind: "message", T: Stage },
{ no: 2, name: "type", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): WatchStageResponse {
Expand Down
13 changes: 13 additions & 0 deletions ui/src/utils/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';

export function useDocumentEvent<T>(event: string, callback: () => T) {
const [value, setValue] = useState<T>(callback());

useEffect(() => {
const handler = () => setValue(callback());
document.addEventListener(event, handler);
return () => document.removeEventListener(event, handler);
}, [event]);

return value;
}

0 comments on commit 292da98

Please sign in to comment.