Skip to content

Commit

Permalink
feat: let user add station name (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
keplervital authored Apr 29, 2024
1 parent 9474260 commit 708e2ae
Show file tree
Hide file tree
Showing 86 changed files with 770 additions and 414 deletions.
7 changes: 5 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ ic-cdk = "0.13.1"
ic-cdk-macros = "0.9"
ic-cdk-timers = "0.7.0"
ic-ledger-types = "0.10.0"
ic-stable-structures = "0.6.0"
ic-stable-structures = "0.6.4"
lazy_static = "1.4.0"
mockall = "0.12.1"
num-bigint = "0.4"
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet/src/components/accounts/TransferDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<VCol :cols="12">
<VTextField
v-model="summary"
:label="$t('terms.summary')"
:label="$t('terms.comment_optional')"
density="comfortable"
class="mb-2"
name="to"
Expand Down
27 changes: 26 additions & 1 deletion apps/wallet/src/components/add-station/DeployStation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { services } from '~/plugins/services.plugin';
import { flushPromises } from '@vue/test-utils';
import { CanDeployStationResponse, User } from '~/generated/control-panel/control_panel.did';
import { Principal } from '@dfinity/principal';
import { StationService } from '~/services/station.service';

vi.mock('~/utils/helper.utils', async importOriginal => {
const mod = (await importOriginal()) as object;
Expand All @@ -22,6 +23,18 @@ vi.mock('~/stores/station.store', async importOriginal => {
};
});

vi.mock('~/services/station.service', () => {
const mock: Partial<StationService> = {
withStationId: vi.fn().mockReturnThis(),
capabilities: vi.fn().mockImplementation(() => Promise.resolve({})),
isHealthy: vi.fn().mockResolvedValue(true),
};

return {
StationService: vi.fn(() => mock),
};
});

describe('DeployStation', () => {
it('renders correctly', () => {
vi.spyOn(services().controlPanel, 'canDeployStation').mockResolvedValueOnce(
Expand Down Expand Up @@ -95,7 +108,7 @@ describe('DeployStation', () => {
expect(wrapper.find('[data-test-id="deploy-quota-exceeded-error"]').exists()).toBe(true);
});

it('will show the deploy screen if the is approved to create a new wallet', async () => {
it('will show the deploy screen if the user is approved to create a new wallet', async () => {
vi.spyOn(services().controlPanel, 'canDeployStation').mockResolvedValue({
Allowed: BigInt(10),
} as CanDeployStationResponse);
Expand All @@ -111,6 +124,18 @@ describe('DeployStation', () => {

await flushPromises();

const form = wrapper.find('[data-test-id="deploy-station-form"]');

form.find('input[name="station_name"]').setValue('test');
form.find('input[name="admin_name"]').setValue('admin');

await flushPromises();

form.trigger('submit');

await wrapper.vm.$nextTick();
await flushPromises();

expect(wrapper.find('[data-test-id="deploying-station"]').exists()).toBe(true);

// will redirect after deploy is complete
Expand Down
84 changes: 77 additions & 7 deletions apps/wallet/src/components/add-station/DeployStation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
type="email"
:rules="[requiredRule, validEmail]"
:label="$t('pages.add_station.join_waitlist_email_field')"
:variant="'outlined'"
variant="filled"
hide-details="auto"
:disabled="working"
data-test-id="join-waitlist-form-email"
Expand All @@ -49,6 +49,57 @@
</div>
</VForm>

<VForm
v-else-if="canDeployStatus === CanDeployStatus.PickName"
ref="stationForm"
class="mt-12"
data-test-id="deploy-station-form"
@submit.prevent="stationFormSubmit"
>
<h2 class="mb-6 text-h4">
{{ $t('pages.add_station.station_title') }}
</h2>
<p class="text-body-1 mb-6">
{{ $t('pages.add_station.station_body') }}
</p>

<VTextField
v-model.trim="stationName"
type="text"
name="station_name"
:rules="[requiredRule, maxLengthRule(40, $t('pages.add_station.station_name_field'))]"
:label="$t('pages.add_station.station_name_field')"
variant="filled"
hide-details="auto"
:disabled="working"
data-test-id="deploy-station-form-name-field"
/>

<VTextField
v-model.trim="adminName"
type="text"
name="admin_name"
class="mt-4"
:rules="[requiredRule, maxLengthRule(50, $t('pages.add_station.admin_name_field'))]"
:label="$t('pages.add_station.admin_name_field')"
variant="filled"
hide-details="auto"
:disabled="working"
data-test-id="deploy-station-form-admin-name-field"
/>

<div class="d-flex align-center ga-4 mt-6">
<VBtn
color="primary"
type="submit"
:loading="working"
:disabled="working || !isStationFormValid"
>
{{ $t('terms.create') }}
</VBtn>
</div>
</VForm>

<div
v-else-if="canDeployStatus === CanDeployStatus.InWaitingList"
class="mt-12"
Expand Down Expand Up @@ -129,6 +180,7 @@ import { useAppStore } from '~/stores/app.store';
import { useSessionStore } from '~/stores/session.store';
import { createUserInitialAccount, useStationStore } from '~/stores/station.store';
import { VFormValidation } from '~/types/helper.types';
import { maxLengthRule } from '~/utils/form.utils';
import { requiredRule, validEmail } from '~/utils/form.utils';
import { unreachable, variantIs, wait } from '~/utils/helper.utils';
Expand All @@ -140,6 +192,7 @@ enum CanDeployStatus {
NotAllowed = 'not_allowed',
Approved = 'approved',
QuotaExceeded = 'quota_exceeded',
PickName = 'pick_name',
}
const canDeployStatus = ref<CanDeployStatus>(CanDeployStatus.CheckPermissions);
Expand Down Expand Up @@ -169,6 +222,11 @@ const working = ref(false);
const form = ref<VFormValidation | null>(null);
const isFormValid = computed(() => (form.value ? form.value.isValid : false));
const stationName = ref('');
const adminName = ref('');
const stationForm = ref<VFormValidation | null>(null);
const isStationFormValid = computed(() => (stationForm.value ? stationForm.value.isValid : false));
const waitUntilStationIsInitialized = async (
stationId: Principal,
{ retries, retryWaitMs }: { retries?: number; retryWaitMs?: number } = {},
Expand Down Expand Up @@ -201,7 +259,10 @@ const waitUntilStationIsInitialized = async (
const deployInitialStation = async (): Promise<void> => {
try {
deploymentStatus.value = DeployStationStatus.Deploying;
const stationId = await controlPanelService.deployStation();
const stationId = await controlPanelService.deployStation({
station_name: stationName.value,
admin_name: adminName.value,
});
const controlPanelUser = await controlPanelService.getCurrentUser();
// wait for the station to be initialized, this requires one round of consensus
Expand Down Expand Up @@ -242,14 +303,25 @@ async function joinWaitlist() {
}
}
async function stationFormSubmit() {
deploymentStatus.value = DeployStationStatus.Starting;
canDeployStatus.value = CanDeployStatus.Approved;
try {
await deployInitialStation();
} catch (e: unknown) {
app.sendErrorNotification(e);
deploymentStatus.value = DeployStationStatus.Failed;
}
}
onMounted(async () => {
try {
const canDeploy = await controlPanelService.canDeployStation();
if (variantIs(canDeploy, 'NotAllowed')) {
if (variantIs(canDeploy.NotAllowed, 'Approved')) {
deploymentStatus.value = DeployStationStatus.Starting;
canDeployStatus.value = CanDeployStatus.Approved;
canDeployStatus.value = CanDeployStatus.PickName;
await deployInitialStation();
} else if (variantIs(canDeploy.NotAllowed, 'Denylisted')) {
canDeployStatus.value = CanDeployStatus.NotAllowed;
Expand All @@ -265,9 +337,7 @@ onMounted(async () => {
}
if (variantIs(canDeploy, 'Allowed')) {
deploymentStatus.value = DeployStationStatus.Starting;
canDeployStatus.value = CanDeployStatus.Approved;
await deployInitialStation();
canDeployStatus.value = CanDeployStatus.PickName;
return;
}
Expand Down
26 changes: 21 additions & 5 deletions apps/wallet/src/components/add-station/JoinStation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ import JoinStation from './JoinStation.vue';
import { useSessionStore } from '~/stores/session.store';
import { flushPromises } from '@vue/test-utils';
import { useAppStore } from '~/stores/app.store';
import { StationService } from '~/services/station.service';

vi.mock('~/services/station.service', () => {
const mock: Partial<StationService> = {
withStationId: vi.fn().mockReturnThis(),
capabilities: vi.fn().mockImplementation(() =>
Promise.resolve({
name: 'test',
}),
),
};

return {
StationService: vi.fn(() => mock),
};
});

describe('JoinStation', () => {
it('renders correctly', () => {
Expand Down Expand Up @@ -45,16 +61,16 @@ describe('JoinStation', () => {

const canisterId = wrapper.find('[data-test-id="join-station-form-canister-id"] input');

// name is optional
expect(
wrapper.find('[data-test-id="join-station-form-canister-name"]').classes(),
).not.toContain('v-input--error');

// canister id is required
expect(wrapper.find('[data-test-id="join-station-form-canister-id"]').classes()).toContain(
'v-input--error',
);

// name is required
expect(wrapper.find('[data-test-id="join-station-form-canister-name"]').classes()).toContain(
'v-input--error',
);

await canisterId.setValue('hello');
await flushPromises();

Expand Down
37 changes: 36 additions & 1 deletion apps/wallet/src/components/add-station/JoinStation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
v-model.trim="name"
:label="$t('pages.add_station.join_station_name')"
data-test-id="join-station-form-canister-name"
:rules="[requiredRule, maxLengthRule(40, $t('pages.add_station.join_station_name'))]"
variant="outlined"
:disabled="working"
/>
Expand All @@ -59,11 +60,16 @@ import { VFormValidation } from '~/types/helper.types';
import { useSessionStore } from '~/stores/session.store';
import { ref } from 'vue';
import { computed } from 'vue';
import { requiredRule, validCanisterId } from '~/utils/form.utils';
import { maxLengthRule, requiredRule, validCanisterId } from '~/utils/form.utils';
import { useRouter } from 'vue-router';
import { defaultHomeRoute } from '~/configs/routes.config';
import { useAppStore } from '~/stores/app.store';
import { copyToClipboard } from '~/utils/app.utils';
import { StationService } from '~/services/station.service';
import { icAgent } from '~/core/ic-agent.core';
import { Principal } from '@dfinity/principal';
import logger from '~/core/logger.core';
import { watch } from 'vue';
const session = useSessionStore();
const router = useRouter();
Expand All @@ -75,11 +81,40 @@ const name = ref('');
const form = ref<VFormValidation | null>(null);
const isFormValid = computed(() => (form.value ? form.value.isValid : false));
const stationService = new StationService(icAgent.get());
const emit = defineEmits<{
(event: 'back', payload: void): void;
}>();
const maybeParseCanisterId = (canisterId: string): Principal | undefined => {
try {
return Principal.fromText(canisterId);
} catch {
return undefined;
}
};
const onChangeCanisterIdMaybeFetchName = async () => {
let stationId = maybeParseCanisterId(canisterId.value);
if (!stationId) {
return;
}
try {
const station = await stationService.withStationId(stationId).capabilities();
name.value = station.name;
} catch (e: unknown) {
logger.warn('Failed to fetch station name', e);
}
};
watch(
() => canisterId.value,
() => onChangeCanisterIdMaybeFetchName(),
);
async function addNewStation() {
if (working.value) {
return;
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet/src/components/inputs/UserAutocomplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ watch(
results => {
const updatedItems = results.map(result => ({
value: result.id,
text: result.name?.[0] ?? result.id,
text: result.name,
}));
updateAvailableItemsList(updatedItems);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const { fetchPolicies } = toRefs(props);
const userList = computed(() => {
const users = autocomplete.results.value.map(user => ({
title: user.name?.[0] ? user.name[0] : user.id,
title: user.name,
value: user.id,
}));
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet/src/components/permissions/SpecificUsersForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ const model = computed({
const userList = computed(() => {
const users = usersAutocomplete.results.value.map(user => ({
title: user.name?.[0] ? user.name[0] : user.id,
title: user.name,
value: user.id,
}));
(model.value.prefilledUsers ?? []).forEach(user => {
if (!users.find(g => g.value === user.id)) {
users.push({
title: user.name?.[0] ? user.name[0] : user.id,
title: user.name,
value: user.id,
});
}
Expand Down
Loading

0 comments on commit 708e2ae

Please sign in to comment.