Skip to content

Commit

Permalink
chore(backport release-0.9): chore(ui): improve stability (#2651)
Browse files Browse the repository at this point in the history
Co-authored-by: Mayursinh Sarvaiya <[email protected]>
Co-authored-by: Kent Rancourt <[email protected]>
  • Loading branch information
3 people authored Oct 4, 2024
1 parent 8b61055 commit 2fc19db
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 31 deletions.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"devDependencies": {
"@eslint/compat": "^1.1.1",
"@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0",
"@tanstack/react-query-devtools": "^5.59.0",
"@types/json-schema": "^7.0.15",
"@types/node": "^22.7.1",
"@types/react": "^18.3.3",
Expand Down
20 changes: 20 additions & 0 deletions ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TransportProvider } from '@connectrpc/connect-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ConfigProvider } from 'antd';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

Expand Down Expand Up @@ -54,6 +55,7 @@ export const App = () => (
</BrowserRouter>
</AuthContextProvider>
</ConfigProvider>
<ReactQueryDevtools />
</QueryClientProvider>
</TransportProvider>
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useForm } from 'react-hook-form';
import YamlEditor from '@ui/features/common/code-editor/yaml-editor-lazy';
import { FieldContainer } from '@ui/features/common/form/field-container';
import { ModalProps } from '@ui/features/common/modal/use-modal';
import { queryCache } from '@ui/features/utils/cache';
import {
createResource,
listAnalysisTemplates
} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { decodeUint8ArrayYamlManifestToJson } from '@ui/utils/decode-raw-data';

import { getAnalysisTemplateYAMLExample } from './utils/analysis-template-example';

Expand All @@ -21,7 +23,16 @@ export const CreateAnalysisTemplateModal = ({ visible, hide, namespace }: Props)
const queryClient = useQueryClient();

const { mutateAsync, isPending } = useMutation(createResource, {
onSuccess: () => hide()
onSuccess: (response) => {
for (const result of response?.results || []) {
if (result?.result?.case === 'createdResourceManifest') {
queryCache.analysisTemplates.add(namespace || '', [
decodeUint8ArrayYamlManifestToJson(result?.result?.value)
]);
}
}
hide();
}
});

const { control, handleSubmit } = useForm({
Expand Down
32 changes: 15 additions & 17 deletions ui/src/features/project/list/create-project-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query';
import { useMutation } from '@connectrpc/connect-query';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQueryClient } from '@tanstack/react-query';
import { Form, Input, Modal, Tabs } from 'antd';
import type { JSONSchema4 } from 'json-schema';
import React from 'react';
Expand All @@ -11,11 +10,10 @@ import { z } from 'zod';
import { YamlEditor } from '@ui/features/common/code-editor/yaml-editor';
import { FieldContainer } from '@ui/features/common/form/field-container';
import { ModalComponentProps } from '@ui/features/common/modal/modal-context';
import { queryCache } from '@ui/features/utils/cache';
import schema from '@ui/gen/schema/projects.kargo.akuity.io_v1alpha1.json';
import {
createResource,
listProjects
} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { createResource } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { decodeUint8ArrayYamlManifestToJson } from '@ui/utils/decode-raw-data';
import { zodValidators } from '@ui/utils/validators';

import { projectYAMLExample } from './utils/project-yaml-example';
Expand All @@ -25,9 +23,15 @@ const formSchema = z.object({
});

export const CreateProjectModal = ({ visible, hide }: ModalComponentProps) => {
const queryClient = useQueryClient();
const { mutateAsync, isPending } = useMutation(createResource, {
onSuccess: () => hide()
onSuccess: (response) => {
for (const result of response?.results || []) {
if (result?.result?.case === 'createdResourceManifest') {
queryCache.project.add([decodeUint8ArrayYamlManifestToJson(result?.result?.value)]);
}
}
hide();
}
});

const { control, handleSubmit, watch, setValue } = useForm({
Expand All @@ -39,15 +43,9 @@ export const CreateProjectModal = ({ visible, hide }: ModalComponentProps) => {

const onSubmit = handleSubmit(async (data) => {
const textEncoder = new TextEncoder();
await mutateAsync(
{
manifest: textEncoder.encode(data.value)
},
{
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: createConnectQueryKey(listProjects) })
}
);
await mutateAsync({
manifest: textEncoder.encode(data.value)
});
});

const yamlValue = watch('value');
Expand Down
4 changes: 3 additions & 1 deletion ui/src/features/project/pipelines/create-warehouse-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const formSchema = z.object({

export const CreateWarehouseModal = ({ visible, hide, project }: Props) => {
const { mutateAsync, isPending } = useMutation(createResource, {
onSuccess: () => hide()
onSuccess: () => {
hide();
}
});

const { control, handleSubmit } = useForm({
Expand Down
15 changes: 9 additions & 6 deletions ui/src/features/project/pipelines/pipelines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,20 @@ export const Pipelines = ({

const client = useQueryClient();

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

const watcher = new Watcher(name, client);
watcher.watchStages(data.stages.slice());
watcher.watchWarehouses(warehouseData?.warehouses || [], refetchFreightData);

return () => watcher.cancelWatch();
}, [isLoading, isVisible, name]);
watcher.watchStages();
watcher.watchWarehouses(refetchFreightData);

return () => {
watcher.cancelWatch();
};
}, [name, client, isVisible]);

const [nodes, connectors, box, sortedStages, stageColorMap, warehouseColorMap] = usePipelineGraph(
name,
Expand Down
24 changes: 19 additions & 5 deletions ui/src/features/project/pipelines/utils/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ import {
listWarehouses
} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { KargoService } from '@ui/gen/service/v1alpha1/service_connect';
import { ListStagesResponse, ListWarehousesResponse } from '@ui/gen/service/v1alpha1/service_pb';
import { Stage, Warehouse } from '@ui/gen/v1alpha1/generated_pb';

async function ProcessEvents<T extends { type: string }, S extends { metadata?: ObjectMeta }>(
stream: AsyncIterable<T>,
data: S[],
getData: () => S[],
getter: (e: T) => S,
callback: (item: S, data: S[]) => void
) {
for await (const e of stream) {
let data = getData();
const index = data.findIndex((item) => item.metadata?.name === getter(e).metadata?.name);
if (e.type === 'DELETED') {
if (index !== -1) {
Expand Down Expand Up @@ -53,15 +55,21 @@ export class Watcher {
this.cancel.abort();
}

async watchStages(stages: Stage[]) {
async watchStages() {
const stream = this.promiseClient.watchStages(
{ project: this.project },
{ signal: this.cancel.signal }
);

ProcessEvents(
stream,
stages,
() => {
const data = this.client.getQueryData(
createConnectQueryKey(listStages, { project: this.project })
);

return (data as ListStagesResponse)?.stages || [];
},
(e) => e.stage as Stage,
(stage, data) => {
// update Stages list
Expand All @@ -78,7 +86,7 @@ export class Watcher {
);
}

async watchWarehouses(warehouses: Warehouse[], refreshHook: () => void) {
async watchWarehouses(refreshHook: () => void) {
const stream = this.promiseClient.watchWarehouses(
{ project: this.project },
{ signal: this.cancel.signal }
Expand All @@ -87,7 +95,13 @@ export class Watcher {

ProcessEvents(
stream,
warehouses,
() => {
const data = this.client.getQueryData(
createConnectQueryKey(listWarehouses, { project: this.project })
);

return (data as ListWarehousesResponse)?.warehouses || [];
},
(e) => e.warehouse as Warehouse,
(warehouse, data) => {
// refetch freight if necessary
Expand Down
4 changes: 3 additions & 1 deletion ui/src/features/stage/create-stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ export const CreateStage = ({
const [tab, setTab] = useState('wizard');

const { mutateAsync, isPending } = useMutation(createResource, {
onSuccess: () => close()
onSuccess: () => {
close();
}
});

const { control, handleSubmit, setValue } = useForm({
Expand Down
28 changes: 28 additions & 0 deletions ui/src/features/utils/cache/analysis-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createConnectQueryKey, createProtobufSafeUpdater } from '@connectrpc/connect-query';

import { queryClient } from '@ui/config/query-client';
import { AnalysisTemplate } from '@ui/gen/rollouts/api/v1alpha1/generated_pb';
import { listAnalysisTemplates } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';

export default {
add: (project: string, templates: AnalysisTemplate[]) => {
queryClient.setQueriesData(
{
queryKey: createConnectQueryKey(listAnalysisTemplates, { project }),
exact: false
},
createProtobufSafeUpdater(listAnalysisTemplates, (prev) => {
let newTemplates = [...(prev?.analysisTemplates || [])];

if (templates?.length > 0) {
newTemplates = newTemplates.concat(templates);
}

return {
...prev,
analysisTemplates: newTemplates
};
})
);
}
};
9 changes: 9 additions & 0 deletions ui/src/features/utils/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// cache invalidation source-of-truth

import analysisTemplates from './analysis-templates';
import project from './project';

export const queryCache = {
project,
analysisTemplates
};
31 changes: 31 additions & 0 deletions ui/src/features/utils/cache/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createConnectQueryKey, createProtobufSafeUpdater } from '@connectrpc/connect-query';

import { queryClient } from '@ui/config/query-client';
import { listProjects } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import { Project } from '@ui/gen/v1alpha1/generated_pb';

export default {
add: (projects: Project[]) => {
queryClient.setQueriesData(
{
queryKey: createConnectQueryKey(listProjects)
// IMPORTANT: createConnectQueryKey returns falsy elements for filters so lets use only static identifiers
.slice(0, 2),
exact: false
},
createProtobufSafeUpdater(listProjects, (prev) => {
let newProjects = [...(prev?.projects || [])];

if (projects?.length > 0) {
newProjects = newProjects.concat(projects);
}

return {
...prev,
total: newProjects.length,
projects: newProjects
};
})
);
}
};
6 changes: 6 additions & 0 deletions ui/src/utils/decode-raw-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import yaml from 'yaml';

type Data = {
result:
| {
Expand All @@ -15,3 +17,7 @@ export const decodeRawData = (data?: Data) =>
new TextDecoder().decode(
data?.result?.case === 'raw' ? (data?.result?.value ?? new Uint8Array()) : new Uint8Array()
);

export const decodeUint8ArrayYamlManifestToJson = <T>(raw: Uint8Array): T => {
return yaml.parse(new TextDecoder().decode(raw));
};

0 comments on commit 2fc19db

Please sign in to comment.