Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make the studio storage reset actually clean everything #648

Merged
merged 14 commits into from
Dec 2, 2024
Merged
5 changes: 2 additions & 3 deletions frontend/src/components/Simulator/ContractInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import PageSection from '@/components/Simulator/PageSection.vue';
import { CheckCircleIcon } from '@heroicons/vue/24/outline';
import EmptyListPlaceholder from '@/components/Simulator/EmptyListPlaceholder.vue';
import { PlusIcon } from '@heroicons/vue/16/solid';
import { useNodeStore } from '@/stores';
import { useWallet, useContractQueries } from '@/hooks';
import { UploadIcon } from 'lucide-vue-next';
Expand Down Expand Up @@ -54,8 +53,8 @@ const { isDeployed, address, contract } = useContractQueries();
You need at least one validator before you can deploy or interact with a
contract.

<RouterLink :to="{ name: 'settings' }"
><Btn secondary tiny class="mt-1">Go to settings</Btn></RouterLink
<RouterLink :to="{ name: 'validators' }"
><Btn secondary tiny class="mt-1">Go to validators</Btn></RouterLink
>
</Alert>

Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/Simulator/ContractItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DocumentPlusIcon,
PencilSquareIcon,
TrashIcon,
ArrowDownOnSquareIcon,
} from '@heroicons/vue/16/solid';
import { nextTick } from 'process';
import { ref, onMounted } from 'vue';
Expand Down Expand Up @@ -87,6 +88,20 @@ const handleRemoveFile = (id: string) => {
title: 'Contract deleted',
});
};

const handleDownloadFile = (e: Event) => {
e.preventDefault();
if (!props.contract?.content) return;
const blob = new Blob([props.contract.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = String(props.contract?.name);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
</script>

<template>
Expand Down Expand Up @@ -136,6 +151,12 @@ const handleRemoveFile = (id: string) => {
/>
</button>

<button @click.stop="handleDownloadFile" v-tooltip="'Download file'">
<ArrowDownOnSquareIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>

<button
@click.stop="deleteModalOpen = true"
v-tooltip="'Delete file'"
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/Simulator/ContractParams.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,19 @@ onMounted(() => {
});
</script>
<template>
<EmptyListPlaceholder v-if="methodBase === undefined || args === undefined">
<EmptyListPlaceholder
v-if="
methodBase === undefined || args === undefined || args.args.length === 0
"
>
No parameters.
</EmptyListPlaceholder>

<div
v-else
class="flex flex-col justify-start gap-1"
:class="false && 'pointer-events-none opacity-60'"
>
<!-- isDeploying was stripped... -->
<div
v-for="([paramName, paramType], i) in methodBase.params || []"
:key="paramName"
Expand Down
58 changes: 31 additions & 27 deletions frontend/src/components/Simulator/settings/SimulatorSection.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
<script setup lang="ts">
import { notify } from '@kyvg/vue3-notification';
import { useNodeStore, useContractsStore } from '@/stores';
import { useTransactionsStore, useContractsStore } from '@/stores';
import { ref } from 'vue';
import PageSection from '@/components/Simulator/PageSection.vue';
import { ArchiveXIcon } from 'lucide-vue-next';
import { ArchiveXIcon, XIcon } from 'lucide-vue-next';
import { useSetupStores } from '@/hooks';

const contractsStore = useContractsStore();
const nodeStore = useNodeStore();
const transactionsStore = useTransactionsStore();
const { setupStores } = useSetupStores();

const isResetStorageModalOpen = ref(false);
const isResetting = ref(false);

const handleResetStorage = async () => {
isResetting.value = true;
try {
await contractsStore.resetStorage();
await transactionsStore.resetStorage();
await setupStores();

notify({
title: 'Storage reset successfully',
Expand All @@ -37,15 +42,7 @@ const handleResetStorage = async () => {
<PageSection>
<template #title>Storage</template>

<Btn
@click="isResetStorageModalOpen = true"
:icon="ArchiveXIcon"
:disabled="nodeStore.contractsToDelete.length < 1"
secondary
v-tooltip="
nodeStore.contractsToDelete.length < 1 && 'No contracts files to delete'
"
>
<Btn @click="isResetStorageModalOpen = true" :icon="ArchiveXIcon" secondary>
Reset Storage
</Btn>

Expand All @@ -59,25 +56,32 @@ const handleResetStorage = async () => {
:confirming="isResetting"
>
<template #title>Reset Studio Storage</template>
<template #description
>Are you sure? All the examples will be restored, and the following
intelligent contracts will be removed.</template
>

<template #info>
<div
class="text-xs"
v-for="contract in nodeStore.contractsToDelete"
:key="contract.id"
>
{{ contract.name }}
</div>
<template #description
>The following items will be deleted from your local storage:
</template>

<div class="mt-1 text-xs italic">
<span class="font-semibold">Note:</span> if you want to preserve any of
these contracts, make a copy of them in the files section.
<div class="mx-auto text-sm font-normal">
<div class="flex flex-row items-center gap-2">
<XIcon :size="16" class="text-red-500" /> All contract files
</div>
<div class="flex flex-row items-center gap-2">
<XIcon :size="16" class="text-red-500" /> All contract deployments
</div>
<div class="flex flex-row items-center gap-2">
<XIcon :size="16" class="text-red-500" /> All transactions
</div>
</div>

<Alert info>
If you want to preserve contracts, download a copy of them from the
<RouterLink
:to="{ name: 'contracts' }"
class="underline"
@click="isResetStorageModalOpen = false"
>contracts section</RouterLink
>.
</Alert>
</ConfirmationModal>
</PageSection>
</template>
46 changes: 6 additions & 40 deletions frontend/src/stores/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import type { ContractFile, DeployedContract } from '@/types';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { notify } from '@kyvg/vue3-notification';
import { useDb, useFileName, useSetupStores } from '@/hooks';
import { useTransactionsStore } from '@/stores';
import { useDb, useFileName } from '@/hooks';

export const useContractsStore = defineStore('contractsStore', () => {
const contracts = ref<ContractFile[]>([]);
const openedFiles = ref<string[]>([]);
const db = useDb();
const { setupStores } = useSetupStores();
const { cleanupFileName } = useFileName();
const transactionsStore = useTransactionsStore();

const currentContractId = ref<string | undefined>(
localStorage.getItem('contractsStore.currentContractId') || '',
Expand Down Expand Up @@ -139,43 +136,12 @@ export const useContractsStore = defineStore('contractsStore', () => {
}

async function resetStorage(): Promise<void> {
try {
const idsToDelete = contracts.value
.filter((c) => c.example)
.map((c) => c.id);

await db.deployedContracts
.where('contractId')
.anyOf(idsToDelete)
.delete();
await db.contractFiles.where('id').anyOf(idsToDelete).delete();

idsToDelete.forEach((id) => {
transactionsStore.clearTransactionsForContract(id);
});

deployedContracts.value = [
...deployedContracts.value.filter(
(c) => !idsToDelete.includes(c.contractId),
),
];
contracts.value = [
...contracts.value.filter((c) => !idsToDelete.includes(c.id)),
];
openedFiles.value = [
...openedFiles.value.filter((c) => !idsToDelete.includes(c)),
];
if (
currentContractId.value &&
idsToDelete.includes(currentContractId.value)
) {
currentContractId.value = '';
}
contracts.value = [];
openedFiles.value = [];
currentContractId.value = '';

await setupStores();
} catch (error) {
console.error(error);
}
await db.deployedContracts.clear();
await db.contractFiles.clear();
}

const currentContract = computed(() => {
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/stores/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import type {
} from '@/types';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useContractsStore } from './contracts';
import { notify } from '@kyvg/vue3-notification';
import { useRpcClient, useWebSocketClient } from '@/hooks';

export const useNodeStore = defineStore('nodeStore', () => {
const rpcClient = useRpcClient();
const webSocketClient = useWebSocketClient();
const logs = ref<NodeLog[]>([]);
const contractsStore = useContractsStore();
const nodeProviders = ref<GetProvidersAndModelsData>([]);
const validators = ref<ValidatorModel[]>([]);
const isLoadingValidatorData = ref<boolean>(true);
Expand Down Expand Up @@ -178,10 +176,6 @@ export const useNodeStore = defineStore('nodeStore', () => {
getProvidersData();
}

const contractsToDelete = computed(() =>
contractsStore.contracts.filter((c) => c.example),
);

const validatorsOrderedById = computed(() =>
validators.value.slice().sort((a, b) => a.id - b.id),
);
Expand Down Expand Up @@ -218,7 +212,6 @@ export const useNodeStore = defineStore('nodeStore', () => {
logs,
validators,
nodeProviders,
contractsToDelete,
isLoadingValidatorData,
isLoadingProviders,
searchFilter,
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/stores/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export const useTransactionsStore = defineStore('transactionsStore', () => {
}

function subscribe(topics: string[]) {
subscriptions.add(topics);
topics.forEach((topic) => {
subscriptions.add(topic);
});
if (webSocketClient.connected) {
webSocketClient.emit('subscribe', topics);
}
Expand All @@ -109,6 +111,12 @@ export const useTransactionsStore = defineStore('transactionsStore', () => {
subscribe(transactions.value.map((t) => t.hash));
}

async function resetStorage() {
transactions.value.forEach((t) => unsubscribe(t.hash));
transactions.value = [];
await db.transactions.clear();
}

return {
transactions,
getTransaction,
Expand All @@ -118,5 +126,6 @@ export const useTransactionsStore = defineStore('transactionsStore', () => {
clearTransactionsForContract,
refreshPendingTransactions,
initSubscriptions,
resetStorage,
};
});
23 changes: 10 additions & 13 deletions frontend/test/unit/stores/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useContractsStore } from '@/stores';
import { useContractsStore, useTransactionsStore } from '@/stores';
import {
useDb,
useFileName,
Expand Down Expand Up @@ -37,21 +37,16 @@ vi.mock('@kyvg/vue3-notification', () => ({

describe('useContractsStore', () => {
let contractsStore: ReturnType<typeof useContractsStore>;
let transactionsStore: ReturnType<typeof useTransactionsStore>;
const mockDb = {
deployedContracts: {
where: vi.fn().mockReturnThis(),
anyOf: vi.fn().mockReturnThis(),
delete: vi.fn(),
clear: vi.fn(),
},
contractFiles: {
where: vi.fn().mockReturnThis(),
anyOf: vi.fn().mockReturnThis(),
delete: vi.fn(),
clear: vi.fn(),
},
transactions: {
where: vi.fn().mockReturnThis(),
equals: vi.fn().mockReturnThis(),
delete: vi.fn().mockResolvedValue(undefined),
clear: vi.fn(),
},
};

Expand All @@ -72,6 +67,7 @@ describe('useContractsStore', () => {
(useWebSocketClient as Mock).mockReturnValue({});

contractsStore = useContractsStore();
transactionsStore = useTransactionsStore();
vi.clearAllMocks();
});

Expand Down Expand Up @@ -132,10 +128,11 @@ describe('useContractsStore', () => {
contractsStore.contracts = exampleContracts;

await contractsStore.resetStorage();
await transactionsStore.resetStorage();

expect(mockDb.deployedContracts.delete).toHaveBeenCalled();
expect(mockDb.contractFiles.delete).toHaveBeenCalled();
expect(mockSetupStores.setupStores).toHaveBeenCalled();
expect(mockDb.deployedContracts.clear).toHaveBeenCalled();
expect(mockDb.contractFiles.clear).toHaveBeenCalled();
expect(mockDb.transactions.clear).toHaveBeenCalled();
expect(contractsStore.contracts).toHaveLength(0);
expect(contractsStore.openedFiles).toHaveLength(0);
});
Expand Down
4 changes: 0 additions & 4 deletions frontend/test/unit/stores/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,6 @@ describe('useNodeStore', () => {
expect(nodeStore.logs).toHaveLength(0);
});

it('should compute contractsToDelete', () => {
expect(nodeStore.contractsToDelete).toEqual([{ id: 1, example: true }]);
});

it('should compute validatorsOrderedById', () => {
nodeStore.validators = [testValidator2, testValidator1];
expect(nodeStore.validatorsOrderedById).toEqual([
Expand Down
Loading
Loading