Skip to content

Commit a20632c

Browse files
authored
feat: implement API cache for the project groups page (#3036)
1 parent 302ae2c commit a20632c

11 files changed

Lines changed: 519 additions & 247 deletions

File tree

File renamed without changes.

frontend/package-lock.json

Lines changed: 122 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"@nangohq/frontend": "^0.9.0",
3535
"@nangohq/frontend-v2": "npm:@nangohq/frontend@^0.52.4",
3636
"@tailwindcss/line-clamp": "^0.4.2",
37+
"@tanstack/vue-query": "^5.75.1",
38+
"@tanstack/vue-query-devtools": "^5.75.1",
3739
"@vitejs/plugin-vue": "^4.2.3",
3840
"@vue/eslint-config-airbnb": "^7.0.0",
3941
"@vuelidate/core": "^2.0.1",

frontend/src/app.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<app-resize-page />
1616
</div>
1717
<lf-globals />
18+
<VueQueryDevtools />
1819
</div>
1920
</template>
2021

@@ -28,13 +29,15 @@ import { useAuthStore } from '@/modules/auth/store/auth.store';
2829
import useSessionTracking from '@/shared/modules/monitoring/useSessionTracking';
2930
import { useLfSegmentsStore } from '@/modules/lf/segments/store';
3031
import LfGlobals from '@/shared/components/globals.vue';
32+
import { VueQueryDevtools } from '@tanstack/vue-query-devtools';
3133
3234
export default {
3335
name: 'App',
3436
3537
components: {
3638
LfGlobals,
3739
AppResizePage,
40+
VueQueryDevtools,
3841
},
3942
4043
setup() {

frontend/src/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { vueSanitizeOptions } from '@/shared/plugins/sanitize';
1717
import marked from '@/shared/plugins/marked';
1818
import { useLogRocket } from '@/utils/logRocket';
1919
import { initRUM } from '@/utils/datadog/rum';
20+
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query';
2021

2122
declare module 'vue' {
2223
interface ComponentCustomProperties {
@@ -34,6 +35,16 @@ declare module 'vue' {
3435
const app = createApp(App);
3536
app.use(pinia);
3637

38+
// Create a client
39+
const queryClient = new QueryClient({
40+
defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } },
41+
});
42+
43+
// Install the VueQuery plugin
44+
app.use(VueQueryPlugin, {
45+
queryClient,
46+
});
47+
3748
const { captureException } = useLogRocket();
3849

3950
const router = await createRouter();

frontend/src/modules/admin/modules/projects/components/form/lf-project-form.vue

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
>
99
<template #content>
1010
<div
11-
v-if="loading"
12-
v-loading="loading"
11+
v-if="isLoading"
12+
v-loading="isLoading"
1313
class="app-page-spinner h-16 !relative !min-h-5"
1414
/>
1515
<div v-else>
@@ -41,10 +41,7 @@
4141
required: 'Slug is required',
4242
}"
4343
>
44-
<el-input
45-
v-model="form.slug"
46-
placeholder="E.g. kubernetes"
47-
/>
44+
<el-input v-model="form.slug" placeholder="E.g. kubernetes" />
4845
</app-form-item>
4946

5047
<!-- Source ID -->
@@ -57,9 +54,7 @@
5754
required: 'Source ID is required',
5855
}"
5956
>
60-
<el-input
61-
v-model="form.sourceId"
62-
/>
57+
<el-input v-model="form.sourceId" />
6358
</app-form-item>
6459

6560
<!-- Status -->
@@ -85,7 +80,10 @@
8580
:value="status.value"
8681
>
8782
<div class="flex items-center gap-3">
88-
<span class="w-1.5 h-1.5 rounded-full" :class="status.color" />{{ status.label }}
83+
<span
84+
class="w-1.5 h-1.5 rounded-full"
85+
:class="status.color"
86+
/>{{ status.label }}
8987
</div>
9088
</el-option>
9189
</el-select>
@@ -104,58 +102,50 @@
104102
<lf-button
105103
type="primary"
106104
size="medium"
107-
:disabled="!hasFormChanged || $v.$invalid || loading"
105+
:disabled="!hasFormChanged || $v.$invalid || isLoading"
108106
@click="onSubmit"
109107
>
110-
{{ isEditForm ? 'Update' : 'Add project' }}
108+
{{ isEditForm ? "Update" : "Add project" }}
111109
</lf-button>
112110
</template>
113111
</app-drawer>
114112
</template>
115113

116-
<script setup>
114+
<script setup lang="ts">
117115
import formChangeDetector from '@/shared/form/form-change';
118116
import useVuelidate from '@vuelidate/core';
119117
import { required, maxLength } from '@vuelidate/validators';
120118
import {
121-
computed, onMounted, reactive, ref,
119+
computed, onMounted, reactive, ref, watch,
122120
} from 'vue';
123121
import AppFormItem from '@/shared/form/form-item.vue';
124122
import statusOptions from '@/modules/lf/config/status';
125-
import { useLfSegmentsStore } from '@/modules/lf/segments/store';
126123
import { useRoute } from 'vue-router';
127124
import useProductTracking from '@/shared/modules/monitoring/useProductTracking';
128-
import { EventType, FeatureEventKey } from '@/shared/modules/monitoring/types/event';
125+
import {
126+
EventType,
127+
FeatureEventKey,
128+
} from '@/shared/modules/monitoring/types/event';
129129
import LfButton from '@/ui-kit/button/Button.vue';
130+
import { Project } from '@/modules/lf/segments/types/Segments';
131+
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
132+
import { TanstackKey } from '@/shared/types/tanstack';
133+
import { segmentService } from '@/modules/lf/segments/segments.service';
134+
import Message from '@/shared/message/message';
130135
131-
const emit = defineEmits(['update:modelValue']);
132-
const props = defineProps({
133-
modelValue: {
134-
type: Boolean,
135-
required: true,
136-
},
137-
id: {
138-
type: String,
139-
default: () => null,
140-
},
141-
parentSlug: {
142-
type: String,
143-
default: () => null,
144-
},
145-
});
136+
const emit = defineEmits<{(e: 'update:modelValue', v: boolean): void;
137+
}>();
138+
139+
const props = defineProps<{
140+
modelValue: boolean;
141+
id?: string | null;
142+
parentSlug: string;
143+
}>();
146144
147145
const route = useRoute();
148146
149147
const { trackEvent } = useProductTracking();
150148
151-
const lsSegmentsStore = useLfSegmentsStore();
152-
const {
153-
createProject,
154-
updateProject,
155-
findProject,
156-
} = lsSegmentsStore;
157-
158-
const loading = ref(false);
159149
const submitLoading = ref(false);
160150
const form = reactive({
161151
name: '',
@@ -191,30 +181,69 @@ const model = computed({
191181
192182
const isEditForm = computed(() => !!props.id);
193183
194-
const fillForm = (record) => {
184+
const fillForm = (record?: Project) => {
195185
if (record) {
196186
Object.assign(form, record);
197187
}
198188
199189
formSnapshot();
200190
};
201191
202-
onMounted(() => {
203-
if (props.id) {
204-
loading.value = true;
192+
const { isLoading, isSuccess, data } = useQuery({
193+
queryKey: [TanstackKey.ADMIN_PROJECT_GROUPS, props.id],
194+
queryFn: () => {
195+
if (!props.id) {
196+
return Promise.resolve(null);
197+
}
198+
return segmentService.getSegmentById(props.id);
199+
},
200+
enabled: !!props.id,
201+
});
205202
206-
findProject(props.id)
207-
.then((response) => {
208-
fillForm(response);
209-
})
210-
.finally(() => {
211-
loading.value = false;
212-
});
213-
} else {
203+
watch(
204+
data,
205+
() => {
206+
if (isSuccess.value && data.value) {
207+
fillForm(data.value as Project);
208+
}
209+
},
210+
{ immediate: true },
211+
);
212+
213+
onMounted(() => {
214+
if (!props.id) {
214215
fillForm();
215216
}
216217
});
217218
219+
const queryClient = useQueryClient();
220+
const onSuccess = () => {
221+
submitLoading.value = false;
222+
model.value = false;
223+
queryClient.invalidateQueries({
224+
queryKey: [TanstackKey.ADMIN_PROJECT_GROUPS],
225+
});
226+
Message.success(`Project ${props.id ? 'updated' : 'created'} successfully`);
227+
};
228+
229+
const onError = () => {
230+
Message.error(
231+
`Something went wrong while ${props.id ? 'updating' : 'creating'} the project`,
232+
);
233+
};
234+
235+
const updateMutation = useMutation({
236+
mutationFn: ({ id, form }: { id: string; form: Project }) => segmentService.updateSegment(id, form),
237+
onSuccess,
238+
onError,
239+
});
240+
241+
const createMutation = useMutation({
242+
mutationFn: (req: { project: Project; segments: string[] }) => segmentService.createProject(req),
243+
onSuccess,
244+
onError,
245+
});
246+
218247
const onCancel = () => {
219248
model.value = false;
220249
};
@@ -227,31 +256,25 @@ const onSubmit = () => {
227256
key: FeatureEventKey.EDIT_PROJECT,
228257
type: EventType.FEATURE,
229258
});
230-
231-
updateProject(props.id, form)
232-
.finally(() => {
233-
submitLoading.value = false;
234-
model.value = false;
235-
});
259+
updateMutation.mutate({
260+
id: props.id!,
261+
form: form as Project,
262+
});
236263
} else {
237264
trackEvent({
238265
key: FeatureEventKey.ADD_PROJECT,
239266
type: EventType.FEATURE,
240267
});
241268
242-
createProject({
243-
...form,
244-
segments: [route.params.id],
245-
})
246-
.finally(() => {
247-
submitLoading.value = false;
248-
model.value = false;
249-
});
269+
createMutation.mutate({
270+
project: form as Project,
271+
segments: [route.params.id as string],
272+
});
250273
}
251274
};
252275
</script>
253276

254-
<script>
277+
<script lang="ts">
255278
export default {
256279
name: 'AppLfProjectForm',
257280
};

0 commit comments

Comments
 (0)