Skip to content

Commit

Permalink
feat: store global user preferences size and format (#3457)
Browse files Browse the repository at this point in the history
Stores user preferences for `size` of listings and `format` of configs
globally in `localStorage`. Usually we store preferences like column
sizes on a route basis. To store preferences "globally" we use the `/`
route (still including the prefix).

Closes #3413

---------

Signed-off-by: schogges <[email protected]>
Signed-off-by: John Cowen <[email protected]>
Co-authored-by: John Cowen <[email protected]>
  • Loading branch information
schogges and johncowen authored Jan 30, 2025
1 parent d5eef60 commit 40bf55a
Show file tree
Hide file tree
Showing 31 changed files with 133 additions and 76 deletions.
30 changes: 30 additions & 0 deletions packages/kuma-gui/features/application/MainNavigation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ Feature: application / MainNavigation
And the URL contains "page=1&size=50"
And the URL doesn't contain "mesh=default"

Scenario: Pagination from localStorage
Given the localStorage
"""
kumahq.kuma-gui:/:
params:
size: 75
"""
When I visit the "/meshes" URL
Then the URL contains "size=75"

Scenario: Format from localStorage
Given the localStorage
"""
kumahq.kuma-gui:/:
params:
format: yaml
"""
And the URL "/meshes/default/meshservices" responds with
"""
body:
items:
- name: monitor-proxy-0.kuma-demo
labels:
kuma.io/display-name: monitor-proxy-0
k8s.kuma.io/namespace: kuma-demo
"""
When I visit the "/meshes/default/services/mesh-services/monitor-proxy-0.kuma-demo" URL
Then the URL contains "format=yaml"
And the "[data-testid='k-code-block']" element exists

Scenario: History navigation
Given the environment
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<DataSource
:src="uri(sources, '/me/:route', {
route: props.name,
}, {
cacheControl: 'no-cache',
})"
@change="resolve"
v-slot="{ data: me }"
Expand Down Expand Up @@ -53,7 +55,7 @@
</DataSource>
</div>
</template>
<script lang="ts" setup generic="T extends Record<string, string | number | boolean> = {}">
<script lang="ts" setup generic="T extends Record<string, string | number | boolean | typeof Number | typeof String> = {}">
import { computed, provide, inject, ref, watch, onBeforeUnmount, reactive, useAttrs } from 'vue'
import { useRoute, useRouter } from 'vue-router'
Expand Down Expand Up @@ -92,17 +94,19 @@ const env = useEnv()
const can = useCan()
const uri = useUri()
let resolve: (resolved: object) => void
const meResponse = new Promise((r) => resolve = r)
const meResponse = new Promise((r) => {
resolve = r
})
const htmlAttrs = useAttrs()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const sym = Symbol('route-view')
type Params = { [P in keyof T]: T[P] }
type PrimitiveParams = Record<string, string | number | boolean>
type Params = { [P in keyof T]: T[P] extends NumberConstructor ? number : T[P] extends StringConstructor ? string : T[P] }
type RouteReplaceParams = Parameters<typeof router['push']>
const setTitle = createTitleSetter(document)
Expand Down Expand Up @@ -173,75 +177,87 @@ const routeView = {
setAttrs([...attributes.values()])
},
}
const routeParams = reactive<Params>(structuredClone(props.params) as Params)
const routeParams = reactive<Params>({} as Params)
// when any URL params change, normalize/validate/default and reset our actual application params
const redirected = ref<boolean>(false)
let local = {}
watch(() => {
return Object.keys(props.params).map((item) => { return route.params[item] || route.query[item] })
}, async () => {
const stored = await meResponse
const me = get(await meResponse, 'params', {})
// merge params in order of importance/priority:
// 1. Anything stored by the user in storage
// 2. URL query params
// 3. URL path params
const params = {
...get(stored, 'params', {}),
...route.query,
...me,
...local,
...Object.fromEntries(Object.entries(route.query).filter(([_, val]) => (typeof val !== 'undefined' && typeof val !== 'string') || val?.length > 0)),
...route.params,
}
// normalize/validate/default all params using the RouteView :params
// 1. Ignore any `?param[]=0` type params, we just take the first one
// 2. Using normalizeUrlParam and the type information from RouteView :params convert things to the correct type i.e. null > false
// 3. Use RouteView :params to set any params that are empty, i.e. use RouteView :params as defaults.
Object.entries({
...props.params,
}).reduce((prev, [prop, def]) => {
Object.entries(props.params).reduce((prev, [prop, def]) => {
const param = urlParam(typeof params[prop] === 'undefined' ? '' : params[prop])
prev[prop] = normalizeUrlParam(param, def)
return prev
}, routeParams as Record<string, string | number | boolean>)
}, routeParams as PrimitiveParams)
// only one first load, if any params are missing from the URL/query
// redirect/add the query params to the URL.
// this ensures any JS applied defaults are reflected in the URL
if(!redirected.value) {
// we only want query params here
const params = Object.entries(routeParams || {}).reduce((prev, [key, value]) => {
if (typeof route.params[key] === 'undefined') {
prev[key] = value
}
return prev
}, {} as Record<string, string | boolean | undefined>)
if (Object.keys(params).length > 0) {
router.replace({
query: cleanQuery(params, route.query),
})
const _params = Object.entries(routeParams || {}).reduce((prev, [key, value]) => {
if (typeof route.params[key] === 'undefined') {
prev[key] = value
}
redirected.value = true
return prev
}, {} as Partial<PrimitiveParams>)
if(Object.keys(_params).length > 0) {
// we only want query params here
router.replace({
query: cleanQuery(_params, route.query),
})
}
redirected.value = true
}, { immediate: true })
type RouteParams = Record<string, string | boolean | number | undefined>
let newParams: RouteParams = {}
const routerPush = (params: RouteParams) => {
router.push({
name: props.name,
query: cleanQuery(params, route.query),
})
newParams = {}
}
const routeUpdate = (params: RouteParams): void => {
let newParams: Partial<PrimitiveParams> = {}
const routeUpdate = (params: Partial<PrimitiveParams>): void => {
newParams = {
...newParams,
...params,
}
// we look for special Number and String RouteView::params and if we find
// them we store them locally and also send them to user storage we store
// them locally, instead of saving them in storage and then refreshing to
// avoid causing any re-renders due to the refreshed DataSource
// TODO: ideally we wouldn't have locally stored state like this, and we
// would instead use a single source of truth which in this case would be
// localStorage
local = Object.entries(params).reduce((prev, [key, value]) => {
if([Number, String].some(item => props.params[key] === item)) {
prev[key] = value
}
return prev
}, {} as Partial<PrimitiveParams>)
if(Object.keys(local).length > 0) {
submit.value({ params: local, $global: true })
}
routerPush(newParams)
}
const routerPush = (params: Partial<PrimitiveParams>) => {
router.push({
query: cleanQuery(params, route.query),
})
newParams = {}
}
const routeReplace = (...args: RouteReplaceParams) => {
router.push(...args)
Expand Down
27 changes: 18 additions & 9 deletions packages/kuma-gui/src/app/application/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import jsYaml from 'js-yaml'

type URLParamDefault = string | number | boolean
type URLParamDefault = string | number | boolean | NumberConstructor | StringConstructor
type URLParamValues = string | number | boolean
type URLParamValue = string | null

export const runInDebug = (func: () => void) => {
Expand Down Expand Up @@ -90,20 +91,28 @@ export const urlParam = function <T extends URLParamValue> (param: T | T[]): T {
}

//
export const normalizeUrlParam = (param: URLParamValue, def: URLParamDefault): URLParamDefault => {
export const normalizeUrlParam = (param: URLParamValue, definition: URLParamDefault): URLParamValues => {
switch (true) {
case typeof def === 'boolean':
return param === null ? true : def
case typeof def === 'number': {
const value = param === null || param.length === 0 ? def : Number(decodeURIComponent(param))
case typeof definition === 'boolean':
return param === null ? true : definition
case definition === Number:
// We are using ?? here because we hardcode the defaults for String/Number
// so they can never be null
return Number(decodeURIComponent(param ?? ''))
case typeof definition === 'number': {
const value = param === null || param.length === 0 ? definition : Number(decodeURIComponent(param))
if (isNaN(value)) {
return Number(def)
return Number(definition)
} else {
return value
}
}
case typeof def === 'string': {
return param === null || param.length === 0 ? def : decodeURIComponent(param)
case definition === String:
// We are using ?? here because we hardcode the defaults for String/Number
// so they can never be null
return decodeURIComponent(String(param ?? ''))
case typeof definition === 'string': {
return param === null || param.length === 0 ? definition : decodeURIComponent(param)
}
}
throw new TypeError('URL parameters can only be string | number | boolean')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="data-plane-list-view"
:params="{
page: 1,
size: 50,
size: Number,
dataplaneType: 'all',
s: '',
mesh: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
codeSearch: '',
codeFilter: false,
codeRegExp: false,
format: 'structured',
format: String,
}"
v-slot="{ route, t }"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
codeSearch: '',
codeFilter: false,
codeRegExp: false,
format: 'structured',
format: String,
}"
v-slot="{ route, t, uri, can }"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="external-service-list-view"
:params="{
page: 1,
size: 50,
size: Number,
mesh: '',
}"
v-slot="{ route, t, me, uri }"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
gateway: '',
listener: '',
page: 1,
size: 50,
size: Number,
s: '',
dataPlane: '',
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="builtin-gateway-list-view"
:params="{
page: 1,
size: 50,
size: Number,
mesh: '',
gateway: '',
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
codeSearch: '',
codeFilter: false,
codeRegExp: false,
format: 'structured',
format: String,
}"
v-slot="{ route, t }"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
mesh: '',
service: '',
page: 1,
size: 50,
size: Number,
s: '',
dataPlane: '',
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="delegated-gateway-list-view"
:params="{
page: 1,
size: 50,
size: Number,
mesh: '',
}"
v-slot="{ route, t, me, uri }"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
codeSearch: '',
codeFilter: false,
codeRegExp: false,
format: 'structured',
format: String,
}"
v-slot="{ route, t, can }"
>
Expand Down
8 changes: 5 additions & 3 deletions packages/kuma-gui/src/app/me/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ export const sources = ({ get, set }: Storage) => {
{
params: {
size: 50,
format: 'structured',
},
},
app,
route,
)
},
'/me/:route/:data': async (params) => {
const json = JSON.parse(params.data)
const res = merge<object>(await get(params.route), json)
set(params.route, res)
const { $global, ...json } = JSON.parse(params.data)
const targetRoute = $global ? '/' : params.route
const res = merge<object>(await get(targetRoute), json)
set(targetRoute, res)
},
})
}
2 changes: 1 addition & 1 deletion packages/kuma-gui/src/app/meshes/views/MeshListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="mesh-list-view"
:params="{
page: 1,
size: 50,
size: Number,
mesh: '',
}"
v-slot="{ route, t, me, uri }"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="policy-detail-view"
:params="{
page: 1,
size: 50,
size: Number,
s: '',
mesh: '',
policy: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name="policy-list-view"
:params="{
page: 1,
size: 50,
size: Number,
mesh: '',
policyPath: '',
policy: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
codeSearch: '',
codeFilter: false,
codeRegExp: false,
format: 'structured',
format: String,
}"
v-slot="{ route, t }"
>
Expand Down
Loading

0 comments on commit 40bf55a

Please sign in to comment.