add creation and registration of an access point

Signed-off-by: garionion <github@entr0py.de>
This commit is contained in:
garionion 2021-04-05 21:10:47 +02:00
parent 80ea692943
commit 736edcd8a6
Signed by: garionion
GPG key ID: 53352FA607FA681A
11 changed files with 568 additions and 26 deletions

101
package-lock.json generated
View file

@ -7,6 +7,7 @@
"": { "": {
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@vueuse/core": "^4.7.0",
"idb-keyval": "^5.0.4", "idb-keyval": "^5.0.4",
"vue": "^3.0.5", "vue": "^3.0.5",
"vue-router": "^4.0.5" "vue-router": "^4.0.5"
@ -214,6 +215,73 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz",
"integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==" "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA=="
}, },
"node_modules/@vueuse/core": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-4.7.0.tgz",
"integrity": "sha512-0Kmo+Gqn47aCg6HHFUvXabD/T5haWyC5pk2PEzaGay9dGE7D+sc05Y1h2MylzcFzRX/2G4anOxSuDqmvQ/GunQ==",
"dependencies": {
"@vueuse/shared": "4.7.0",
"vue-demi": "latest"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.7.4.tgz",
"integrity": "sha512-PT0qzI9Rp8R8eUAsTPXADC+KAZdrMufmbZqEMMqvaiesWyjCpgyuuvS5Su8cvBjK9RevLT/YfFiKWxc8YB9/8g==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-beta.1",
"vue": "^2.6.0 || >=3.0.0-rc.1"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/shared": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-4.7.0.tgz",
"integrity": "sha512-a9wmH6g+dh6ALeOejIL53s1HkASyOldbHunwEUEtRdgQyUCnU+RRiYTZlNLEyt1r79kPtnBjp5fHq0X36H96MA==",
"dependencies": {
"vue-demi": "latest"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.7.4.tgz",
"integrity": "sha512-PT0qzI9Rp8R8eUAsTPXADC+KAZdrMufmbZqEMMqvaiesWyjCpgyuuvS5Su8cvBjK9RevLT/YfFiKWxc8YB9/8g==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-beta.1",
"vue": "^2.6.0 || >=3.0.0-rc.1"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -2337,6 +2405,39 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz",
"integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==" "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA=="
}, },
"@vueuse/core": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-4.7.0.tgz",
"integrity": "sha512-0Kmo+Gqn47aCg6HHFUvXabD/T5haWyC5pk2PEzaGay9dGE7D+sc05Y1h2MylzcFzRX/2G4anOxSuDqmvQ/GunQ==",
"requires": {
"@vueuse/shared": "4.7.0",
"vue-demi": "latest"
},
"dependencies": {
"vue-demi": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.7.4.tgz",
"integrity": "sha512-PT0qzI9Rp8R8eUAsTPXADC+KAZdrMufmbZqEMMqvaiesWyjCpgyuuvS5Su8cvBjK9RevLT/YfFiKWxc8YB9/8g==",
"requires": {}
}
}
},
"@vueuse/shared": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-4.7.0.tgz",
"integrity": "sha512-a9wmH6g+dh6ALeOejIL53s1HkASyOldbHunwEUEtRdgQyUCnU+RRiYTZlNLEyt1r79kPtnBjp5fHq0X36H96MA==",
"requires": {
"vue-demi": "latest"
},
"dependencies": {
"vue-demi": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.7.4.tgz",
"integrity": "sha512-PT0qzI9Rp8R8eUAsTPXADC+KAZdrMufmbZqEMMqvaiesWyjCpgyuuvS5Su8cvBjK9RevLT/YfFiKWxc8YB9/8g==",
"requires": {}
}
}
},
"acorn": { "acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",

View file

@ -8,6 +8,7 @@
"format": "npx prettier --write ." "format": "npx prettier --write ."
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^4.7.0",
"idb-keyval": "^5.0.4", "idb-keyval": "^5.0.4",
"vue": "^3.0.5", "vue": "^3.0.5",
"vue-router": "^4.0.5" "vue-router": "^4.0.5"

View file

@ -3,7 +3,9 @@ import {
AccessPointCreateResponse, AccessPointCreateResponse,
cSite, cSite,
LoginResponse, LoginResponse,
RegisterAccessPointResponse,
Site, Site,
StateOfTask,
token, token,
} from "./types"; } from "./types";
import { get as idbGet, set as idbSet, del as idbDel } from "idb-keyval"; import { get as idbGet, set as idbSet, del as idbDel } from "idb-keyval";
@ -105,6 +107,10 @@ export async function getSites(): Promise<Site[]> {
return request<Site[]>("/sites", { auth: true, method: HTTPMethod.GET }); return request<Site[]>("/sites", { auth: true, method: HTTPMethod.GET });
} }
export async function getSite(id: number): Promise<Site> {
return request<Site>(`/sites/${id}`, { auth: true, method: HTTPMethod.GET });
}
export async function createSite(site: Site) { export async function createSite(site: Site) {
return request<cSite>( return request<cSite>(
"/sites", "/sites",
@ -134,8 +140,38 @@ export async function createAccessPoint(
ap: AccessPoint ap: AccessPoint
): Promise<AccessPointCreateResponse> { ): Promise<AccessPointCreateResponse> {
return request<AccessPointCreateResponse>( return request<AccessPointCreateResponse>(
"/aps", `/aps${ap.id ? "/" + ap.id : ""}`,
{ auth: true, method: HTTPMethod.POST }, { auth: true, method: ap.id ? HTTPMethod.PATCH : HTTPMethod.POST },
ap Object.fromEntries(
Object.entries(ap).filter(([k, v]) =>
[
"ap_name",
"site_id",
"mac_address",
"serialnumber",
"location",
"model",
"group",
"appointment",
"comment",
].includes(k)
)
)
); );
} }
export async function registerAccessPoint(
apName: string
): Promise<RegisterAccessPointResponse> {
return request<RegisterAccessPointResponse>(
`/${import.meta.env.DEV ? "test" : "register-ap"}/${apName}`,
{ auth: true, method: HTTPMethod.POST }
);
}
export async function getStateOfTask(id: string): Promise<StateOfTask> {
return request<StateOfTask>(`/tasks/${id}`, {
auth: true,
method: HTTPMethod.GET,
});
}

View file

@ -21,8 +21,8 @@ export interface cSite {
} }
export interface AccessPoint { export interface AccessPoint {
id: number; id?: number;
site_id: number; site_id?: number;
ap_name: string; ap_name: string;
serialnumber: string; serialnumber: string;
comment: string; comment: string;
@ -30,7 +30,7 @@ export interface AccessPoint {
location: string; location: string;
mac_address: string; mac_address: string;
model: string; model: string;
appointment: Date; appointment?: Date;
new_switchport_id?: any; new_switchport_id?: any;
new_userport_id?: any; new_userport_id?: any;
old_switchport_id?: any; old_switchport_id?: any;
@ -42,3 +42,23 @@ export interface AccessPoint {
export interface AccessPointCreateResponse { export interface AccessPointCreateResponse {
"ap-id": number; "ap-id": number;
} }
export interface RegisterAccessPointResponse {
status: string;
"task-id": string;
}
export interface StateOfTask {
state: string;
current: string;
total: number;
status: string;
result?: TaskResult;
}
export interface TaskResult {
switchportid: number;
switchportid_new: number;
userportid: number;
userportid_new: number;
}

View file

@ -150,6 +150,14 @@ export default {
name: "AccessPoints", name: "AccessPoints",
to: "AccessPoints", to: "AccessPoints",
}, },
{
name: "Create AccessPoint",
to: "CreateAccessPoint",
},
{
name: "Register AccessPoint",
to: "RegisterAccessPoint",
},
]); ]);
const route = useRoute(); const route = useRoute();
const activeRoute = computed(() => route.name); const activeRoute = computed(() => route.name);

View file

@ -0,0 +1,70 @@
<template>
<div
class="border-yellow-300 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg text-left"
>
<span class="block w-full">{{ apName }}</span>
<div v-if="state.result === undefined">
<span class="block w-full">{{ state.state }}</span>
<progress
class="block w-full"
:max="state.total"
:value="state.current"
></progress>
<span class="block w-full">{{ state.status }}</span>
</div>
<div v-else class="w-full flex">
<span class="flex-1 mr-2"
>SwitchPortID {{ state.result.switchportid }}</span
>
<span class="flex-1 mr-2"
>New SwitchPortID {{ state.result.switchportid_new }}</span
>
<span class="flex-1 mr-2">UserPortID {{ state.result.userportid }}</span>
<span class="flex-1 mr-2"
>SwitchPortID {{ state.result.userportid_new }}</span
>
</div>
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from "vue";
import { useIntervalFn } from "@vueuse/core";
import { getStateOfTask, StateOfTask } from "../api";
export default {
name: "VTaskState",
emits: ["finished"],
props: {
taskID: {
type: String,
required: true,
},
apName: {
type: String,
required: true,
},
},
setup(props: { taskID: string }) {
const { taskID } = toRefs(props);
const state = reactive<StateOfTask>({
state: "",
current: "",
total: undefined,
result: undefined,
status: null,
});
const { pause, resume } = useIntervalFn(async () => {
const s = await getStateOfTask(taskID.value);
Object.assign(state, s);
if (s.result !== undefined) pause();
}, 300);
return { state };
},
};
</script>
<style scoped></style>

View file

@ -4,6 +4,8 @@ const Settings = () => import("../views/Settings.vue");
const Login = () => import("../views/Login.vue"); const Login = () => import("../views/Login.vue");
const Sites = () => import("../views/Sites.vue"); const Sites = () => import("../views/Sites.vue");
const AccessPoints = () => import("../views/AccessPoints.vue"); const AccessPoints = () => import("../views/AccessPoints.vue");
const CreateAccessPoint = () => import("../views/CreateAccessPoint.vue");
const RegisterAccessPoint = () => import("../views/RegisterAccessPoint.vue");
import { isLoggedIn } from "../api"; import { isLoggedIn } from "../api";
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
@ -32,7 +34,28 @@ const routes: RouteRecordRaw[] = [
path: "/aps", path: "/aps",
name: "AccessPoints", name: "AccessPoints",
component: AccessPoints, component: AccessPoints,
props: (route) => ({ site: Number(route.query["site-id"]).valueOf() }), props: (route) => ({ siteID: Number(route.query["site-id"]).valueOf() }),
},
{
path: "/createAccessPoint",
name: "CreateAccessPoint",
component: CreateAccessPoint,
props: (route) => ({ siteID: Number(route.query["site-id"]).valueOf() }),
},
{
path: "/editAccessPoint",
name: "EditAccessPoint",
component: CreateAccessPoint,
props: (route) => ({
apID: Number(route.query["ap-id"]).valueOf(),
edit: true,
}),
},
{
path: "/registerAccessPoint",
name: "RegisterAccessPoint",
component: RegisterAccessPoint,
props: (route) => ({ apID: Number(route.query["ap-id"]).valueOf() }),
}, },
]; ];

View file

@ -26,11 +26,18 @@
<td class="border px-8 py-4">{{ ap.model }}</td> <td class="border px-8 py-4">{{ ap.model }}</td>
<td class="border px-8 py-4">{{ ap.comment }}</td> <td class="border px-8 py-4">{{ ap.comment }}</td>
<td> <td>
<!--router-link <router-link
:to="{ name: 'AccessPoints', query: { 'site-id': site.id } }" :to="{ name: 'EditAccessPoint', query: { 'ap-id': ap.id } }"
class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium" class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium"
>Show APs</router-link >Edit</router-link
--></td> >
<router-link
v-if="ap.registered_user_id == null"
:to="{ name: 'RegisterAccessPoint', query: { 'ap-id': ap.id } }"
class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium"
>Register</router-link
>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -38,24 +45,28 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, toRefs } from "vue"; import { reactive, toRefs } from "vue";
import { getAccessPointDetails, AccessPoint } from "../api"; import { getAccessPointDetails, AccessPoint } from "../api";
export default { export default {
name: "AccessPoints", name: "AccessPoints",
props: { props: {
site: { siteID: {
type: Number, type: Number,
required: false, required: false,
}, },
}, },
async setup(props: { site?: number }) { async setup(props: { siteID?: number }) {
const { site } = toRefs(props); const { siteID } = toRefs(props);
const aps = ref<AccessPoint[]>([]); const aps = reactive<AccessPoint[]>([]);
async function getAccessPoints() { async function getAccessPoints() {
await getAccessPointDetails(site?.value).then( try {
(a) => void (aps.value = a) await getAccessPointDetails(siteID?.value).then(
(a) => void Object.assign(aps, a)
); );
} catch (e) {
console.error(e);
}
} }
await getAccessPoints(); await getAccessPoints();

View file

@ -0,0 +1,189 @@
<template>
<div class="sm:w-3/4 md:w-1/3 w-full dark:text-white">
<h1 class="font-hairline mb-6 text-center">
{{ isEdit ? "Edit" : "Create" }} Access Point
</h1>
<form
class="border-yellow-300 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
@submit.prevent="createAccessPoint"
>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>AccessPoint Name aruba-ap-<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.ap_name"
placeholder="c01"
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>MAC
<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.mac_address"
placeholder="a1b2c3d4e5f6"
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>S/N
<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.serialnumber"
placeholder="12345678"
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>Location
<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.location"
placeholder="02-02-02"
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>Model
<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.model"
placeholder="ap-205h"
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>Appointment
<input
type="text"
disabled
class="text-gray-800 block appearance-none w-full bg-white border border-gray-300 px-2 py-2 rounded shadow"
v-model.trim="ap.appointment"
placeholder=""
/>
</label>
</div>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>Comment
<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="ap.comment"
placeholder=""
/>
</label>
</div>
<div class="flex items-center justify-between">
<button
class="bg-green-600 hover:bg-green-400 text-white font-bold py-2 px-4 rounded"
type="submit"
>
{{ isEdit ? "Edit" : "Create" }}!
</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive } from "vue";
import {
AccessPoint,
getAccessPointByID,
Site,
getSite as apiGetSite,
getSites as apiGetSites,
createAccessPoint as apiCreateAccessPoint,
} from "../api";
import AccessPoints from "./AccessPoints.vue";
import router from "../router";
export default {
name: "CreateAccessPoint",
props: {
siteID: {
type: Number,
required: false,
},
apID: {
type: Number,
required: false,
},
edit: {
type: Boolean,
required: false,
},
},
async setup(props: { siteID?: number; apID?: number; edit?: boolean }) {
const { siteID, apID, edit } = toRefs(props);
const isEdit = ref<boolean>(false);
if (edit?.value === true) isEdit.value = true;
const ap = reactive<AccessPoint>({
ap_name: "",
site_id: siteID?.value,
mac_address: "",
serialnumber: "",
location: "",
model: "",
group: "fem-ap-group",
appointment: undefined,
comment: "",
});
async function getAccessPointSettings(id: number) {
try {
await getAccessPointByID(id).then((a) => Object.assign(ap, a));
} catch (e) {
console.error(e);
}
}
if (apID?.value !== undefined) getAccessPointSettings(apID.value);
const sites = reactive<Site[]>([]);
async function getSite(id: number) {
try {
await apiGetSite(id).then((s) => Object.assign(sites, [s]));
} catch (e) {
console.error(e);
}
}
async function getSites() {
try {
await apiGetSites().then((s) => Object.assign(sites, s));
} catch (e) {
console.error(e);
}
}
if (siteID?.value !== undefined) {
getSite(siteID.value);
} else {
getSites();
}
async function createAccessPoint() {
try {
await apiCreateAccessPoint(ap);
router.push({ name: "AccessPoints" });
} catch (e) {
console.error(e);
}
}
return { isEdit, ap, sites, getSites, createAccessPoint };
},
};
</script>
<style scoped></style>

View file

@ -0,0 +1,83 @@
<template>
<div class="sm:w-3/4 md:w-1/3 w-full dark:text-white">
<h1 class="font-hairline mb-6 text-center">Register Access Point</h1>
<form
class="border-yellow-300 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
@submit.prevent="registerAccessPoint"
>
<div class="mb-4">
<label class="font-bold text-gray-800 block mb-2"
>AccessPoint Name aruba-ap-<input
type="text"
class="text-black block appearance-none w-full bg-white border border-gray-300 hover:border-gray-700 px-2 py-2 rounded shadow"
v-model.trim="apName"
placeholder="c01"
/>
</label>
</div>
<div class="flex items-center justify-between">
<button
class="bg-green-600 hover:bg-green-400 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Register!
</button>
</div>
</form>
</div>
<VTaskState
v-for="task in tasks"
:ap-name="task.apName"
:task-i-d="task.taskID"
></VTaskState>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from "vue";
import {
getAccessPointByID,
registerAccessPoint as apiRegisterAccessPoint,
} from "../api";
import VTaskState from "../components/VTaskState.vue";
export default {
name: "RegisterAccessPoint",
components: { VTaskState },
props: {
apID: {
type: Number,
required: false,
},
},
setup(props: { apID?: number }) {
const { apID } = toRefs(props);
const apName = ref("");
const tasks = reactive<{ apName: string; taskID: string }[]>([]);
const finishedTasks = reactive<{ apName: string; taskID: string }[]>([]);
async function getAccessPointSettings(id: number) {
try {
await getAccessPointByID(id).then((a) => (apName.value = a.ap_name));
} catch (e) {
console.error(e);
}
}
if (!isNaN(apID?.value)) {
getAccessPointSettings(apID.value);
}
async function registerAccessPoint() {
try {
const r = await apiRegisterAccessPoint(apName.value);
tasks.push({ apName: apName.value, taskID: r["task-id"] });
} catch (e) {
console.error(e);
}
}
return { apName, registerAccessPoint, tasks, finishedTasks };
},
};
</script>
<style scoped></style>

View file

@ -25,11 +25,11 @@
<td class="border px-8 py-4">{{ site.id }}</td> <td class="border px-8 py-4">{{ site.id }}</td>
<td class="border px-8 py-4">{{ site.default_prefix }}</td> <td class="border px-8 py-4">{{ site.default_prefix }}</td>
<td> <td>
<!--router-link <router-link
:to="{ name: 'Login', query: { 'site-id': site.id } }" :to="{ name: 'CreateAccessPoint', query: { 'site-id': site.id } }"
class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium" class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium"
>Create AP</router-link >Create AP</router-link
--> >
<router-link <router-link
:to="{ name: 'AccessPoints', query: { 'site-id': site.id } }" :to="{ name: 'AccessPoints', query: { 'site-id': site.id } }"
class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium" class="text-white bg-blue-600 hover:bg-blue-800 px-3 py-2 rounded-md text-sm font-medium"
@ -79,14 +79,14 @@ import {
Site, Site,
createSite as apiCreateSite, createSite as apiCreateSite,
} from "../api"; } from "../api";
import { ref } from "vue"; import { reactive, ref } from "vue";
export default { export default {
name: "Sites", name: "Sites",
async setup() { async setup() {
const sites = ref<Site[]>([]); const sites = reactive<Site[]>([]);
async function getSites() { async function getSites() {
await apiGetSites().then((s) => void (sites.value = s)); await apiGetSites().then((s) => void Object.assign(sites, s));
} }
await getSites(); await getSites();