-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(wallet): Approval Policy aka Named Rules UI (#488)
changes - full frontend support for named rules aka Approval policies. - Approval policies page - rule summary table column for both request policies and approval policies - slight change to named rules API: edit named rule input will take `opt opt text` for description potential todo: - [ ] put named rules on a tab on the request policies page
- Loading branch information
Showing
57 changed files
with
3,043 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<template> | ||
<VTooltip :text="text" location="bottom"> | ||
<template #activator="{ props: activatorProps }"> | ||
<VIcon class="ml-2" v-bind="activatorProps" :icon="icon" /> | ||
</template> | ||
</VTooltip> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { mdiAlertCircleOutline } from '@mdi/js'; | ||
import { VIcon } from 'vuetify/components'; | ||
withDefaults( | ||
defineProps<{ | ||
text: string; | ||
icon?: string; | ||
}>(), | ||
{ | ||
icon: mdiAlertCircleOutline, | ||
}, | ||
); | ||
</script> |
55 changes: 55 additions & 0 deletions
55
apps/wallet/src/components/request-policies/InteractiveNamedRule.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<template> | ||
<template v-if="loading"> | ||
<VProgressCircular indeterminate /> | ||
</template> | ||
<template v-else-if="error !== null"> {{ id }} <ErrorTooltip :text="error" /> </template> | ||
<template v-else-if="namedRule"> | ||
<VTooltip v-if="tooltip" location="bottom" content-class="white-space-pre-wrap" :text="tooltip"> | ||
<template #activator="{ props: activatorProps }"> | ||
<span v-bind="activatorProps" class="underline-dotted font-weight-bold"> | ||
{{ namedRule.name }} | ||
</span> | ||
</template> | ||
</VTooltip> | ||
</template> | ||
<template v-else> | ||
{{ id }} | ||
</template> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { ref } from 'vue'; | ||
import { onMounted } from 'vue'; | ||
import { computed } from 'vue'; | ||
import { toRefs } from 'vue'; | ||
import { useRuleToTooltip } from '~/composables/request-policies.composable'; | ||
import { NamedRule, UUID } from '~/generated/station/station.did'; | ||
import { services } from '~/plugins/services.plugin'; | ||
import { useAppStore } from '~/stores/app.store'; | ||
import { getErrorMessage } from '~/utils/error.utils'; | ||
const input = defineProps<{ | ||
id: UUID; | ||
}>(); | ||
const props = toRefs(input); | ||
const namedRule = ref<NamedRule | null>(null); | ||
const loading = ref(true); | ||
const error = ref<string | null>(null); | ||
const appStore = useAppStore(); | ||
const rule = computed(() => (namedRule.value ? namedRule.value.rule : null)); | ||
const tooltip = useRuleToTooltip(rule); | ||
onMounted(async () => { | ||
loading.value = true; | ||
try { | ||
namedRule.value = (await services().station.getNamedRule(props.id.value)).named_rule; | ||
} catch (e) { | ||
appStore.sendErrorNotification(e); | ||
error.value = getErrorMessage(e); | ||
} finally { | ||
loading.value = false; | ||
} | ||
}); | ||
</script> |
184 changes: 184 additions & 0 deletions
184
apps/wallet/src/components/request-policies/NamedRuleDialog.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { flushPromises } from '@vue/test-utils'; | ||
import { describe, expect, it, vi } from 'vitest'; | ||
import { services } from '~/plugins/services.plugin'; | ||
import { StationService } from '~/services/station.service'; | ||
import { mount } from '~/test.utils'; | ||
import NamedRuleDialog from './NamedRuleDialog.vue'; | ||
import NamedRuleForm from './NamedRuleForm.vue'; | ||
import { VCard } from 'vuetify/components'; | ||
|
||
vi.mock('~/services/station.service', () => { | ||
const mock: Partial<StationService> = { | ||
withStationId: vi.fn().mockReturnThis(), | ||
getNamedRule: vi.fn().mockImplementation(() => | ||
Promise.resolve({ | ||
named_rule: { | ||
id: '1', | ||
name: 'Test Rule', | ||
description: ['Test Description'], | ||
rule: { | ||
AutoApproved: null, | ||
}, | ||
}, | ||
}), | ||
), | ||
addNamedRule: vi.fn().mockImplementation(() => Promise.resolve({} as Request)), | ||
editNamedRule: vi.fn().mockImplementation(() => Promise.resolve({} as Request)), | ||
listNamedRules: vi.fn().mockImplementation(() => | ||
Promise.resolve({ | ||
named_rules: [], | ||
}), | ||
), | ||
}; | ||
|
||
return { | ||
StationService: vi.fn(() => mock), | ||
}; | ||
}); | ||
|
||
describe('NamedRuleDialog', () => { | ||
it('renders correctly', () => { | ||
const wrapper = mount(NamedRuleDialog, { | ||
props: { | ||
open: true, | ||
}, | ||
}); | ||
expect(wrapper.exists()).toBe(true); | ||
}); | ||
|
||
it('loads and displays existing named rule', async () => { | ||
const wrapper = mount(NamedRuleDialog, { | ||
props: { | ||
open: true, | ||
namedRuleId: '1', | ||
}, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
// expect getNamedRule to be called | ||
expect(services().station.getNamedRule).toHaveBeenCalledWith('1'); | ||
|
||
const form = wrapper.findComponent(NamedRuleForm); | ||
|
||
const name = form.find('input[name="name"]').element as HTMLInputElement; | ||
const description = form.find('input[name="description"]').element as HTMLInputElement; | ||
|
||
expect(name.value).toBe('Test Rule'); | ||
expect(description.value).toBe('Test Description'); | ||
}); | ||
|
||
it('creates new named rule', async () => { | ||
const wrapper = mount(NamedRuleDialog, { | ||
props: { | ||
open: true, | ||
}, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
const dialogContents = wrapper.findComponent(VCard); | ||
|
||
const form = wrapper.findComponent(NamedRuleForm); | ||
|
||
// Fill out form | ||
await form.find('input[name="name"]').setValue('New Rule'); | ||
await form.find('input[name="description"]').setValue('New Description'); | ||
|
||
// Set a simple rule | ||
const ruleBuilder = form.findComponent({ name: 'RuleBuilder' }); | ||
await ruleBuilder.vm.$emit('update:modelValue', { | ||
AutoApproved: null, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
// Find and click save button | ||
const saveButton = dialogContents.find('button[data-test-id="save-named-rule"]'); | ||
expect(saveButton.exists()).toBe(true); | ||
await saveButton.trigger('click'); | ||
|
||
await flushPromises(); | ||
|
||
// Verify addNamedRule was called with correct data | ||
expect(services().station.addNamedRule).toHaveBeenCalledWith({ | ||
name: 'New Rule', | ||
description: ['New Description'], | ||
rule: { | ||
AutoApproved: null, | ||
}, | ||
}); | ||
}); | ||
|
||
it('edits existing named rule', async () => { | ||
const wrapper = mount(NamedRuleDialog, { | ||
props: { | ||
open: true, | ||
namedRuleId: '1', | ||
}, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
const dialogContents = wrapper.findComponent(VCard); | ||
|
||
const form = wrapper.findComponent(NamedRuleForm); | ||
|
||
// Update form fields | ||
await form.find('input[name="name"]').setValue('Updated Rule'); | ||
await form.find('input[name="description"]').setValue('Updated Description'); | ||
|
||
// Update rule | ||
const ruleBuilder = form.findComponent({ name: 'RuleBuilder' }); | ||
await ruleBuilder.vm.$emit('update:modelValue', { | ||
AutoApproved: null, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
// Find and click save button | ||
const saveButton = dialogContents.find('button[data-test-id="save-named-rule"]'); | ||
await saveButton.trigger('click'); | ||
|
||
await flushPromises(); | ||
|
||
// Verify editNamedRule was called with correct data | ||
expect(services().station.editNamedRule).toHaveBeenCalledWith({ | ||
named_rule_id: '1', | ||
name: ['Updated Rule'], | ||
description: [['Updated Description']], | ||
rule: [ | ||
{ | ||
AutoApproved: null, | ||
}, | ||
], | ||
}); | ||
}); | ||
|
||
it('handles readonly mode correctly', async () => { | ||
const wrapper = mount(NamedRuleDialog, { | ||
props: { | ||
open: true, | ||
namedRuleId: '1', | ||
readonly: true, | ||
}, | ||
}); | ||
|
||
await flushPromises(); | ||
|
||
const dialogContents = wrapper.findComponent(VCard); | ||
|
||
const form = wrapper.findComponent(NamedRuleForm); | ||
|
||
// Verify inputs are disabled | ||
const nameInput = form.find('input[name="name"]'); | ||
expect(nameInput.attributes('disabled')).toBeDefined(); | ||
|
||
const descriptionInput = form.find('input[name="description"]'); | ||
expect(descriptionInput.attributes('disabled')).toBeDefined(); | ||
|
||
// Verify save button is not present | ||
const saveButton = dialogContents.find('button[data-test-id="save-named-rule"]'); | ||
expect(saveButton.exists()).toBe(false); | ||
}); | ||
}); |
Oops, something went wrong.