Add first version of the frontend
As of now the only usable functionality is signin in/out and managing
authorized users, and even then there is much room for improvement.
Change-Id: Ib87fc6866f69113a230187710de8644b78391917
diff --git a/frontend/src/pages/AuthorizedUsers.vue b/frontend/src/pages/AuthorizedUsers.vue
new file mode 100644
index 0000000..2f2b326
--- /dev/null
+++ b/frontend/src/pages/AuthorizedUsers.vue
@@ -0,0 +1,170 @@
+<script>
+import NotAuthorized from './NotAuthorized.vue';
+import Page from './utils/Page.vue';
+import AuthorizedUserDialog from '../components/AuthorizedUserDialog.vue';
+
+//import * as grpcWeb from "grpc-web";
+import * as ksObjectsPb from '../api_proto/kill_switch_objects_pb.js';
+import * as ksPb from '../api_proto/kill_switch_pb.js';
+import {accessLevels} from '../consts.js';
+
+export default {
+ data() {
+ let emptyUser = new ksObjectsPb.KillSwitchAuthorizedUser();
+
+ return {
+ users: [],
+ currentUpdateUser: emptyUser, // Current user being updated
+ currentDeleteUser: emptyUser, // Current user being confirmed deletion
+ addDialogOpen: false,
+ updateDialogOpen: false,
+ deleteDialogOpen: false,
+ accessLevels,
+ };
+ },
+ components: {
+ NotAuthorized,
+ Page,
+ AuthorizedUserDialog,
+ },
+ mounted() {
+ this.loadData();
+ },
+ methods: {
+ loadData() {
+ if (!this.isSignedIn) return;
+
+ let request = new ksPb.ListAuthorizedUsersRequest();
+
+ this.$store.state.client.listAuthorizedUsers(
+ request, {authorization: this.$store.state.jwtToken})
+ .then(res => {
+ this.$data.users = res.getUsersList();
+ })
+ .catch(err => console.error(err));
+ },
+ showAddForm(user) {
+ this.$data.addDialogOpen = true;
+ },
+ showUpdateForm(user) {
+ this.currentUpdateUser = user;
+ this.$data.updateDialogOpen = true;
+ },
+ showDeleteDialog(user) {
+ this.$data.currentDeleteUser = user;
+ this.$data.deleteDialogOpen = true;
+ },
+ deleteUser(user) {
+ let request = new ksPb.DeleteAuthorizedUserRequest();
+ request.setUserId(user.getId());
+
+ this.$store.state.client.deleteAuthorizedUser(
+ request, {authorization: this.$store.state.jwtToken})
+ .then(res => {
+ this.loadData();
+ })
+ .catch(err => console.error(err));
+ },
+ },
+ computed: {
+ isSignedIn() {
+ return this.$store.state.jwtToken != null;
+ },
+ },
+};
+</script>
+
+<template>
+ <template v-if="isSignedIn">
+ <div class="container">
+ <mcw-data-table>
+ <table class="mdc-data-table__table" aria-label="Authorized users">
+ <thead>
+ <tr class="mdc-data-table__header-row">
+ <th
+ class="mdc-data-table__header-cell"
+ role="columnheader"
+ scope="col">
+ ID
+ </th>
+ <th
+ class="mdc-data-table__header-cell"
+ role="columnheader"
+ scope="col">
+ Google UID
+ </th>
+ <th
+ class="mdc-data-table__header-cell"
+ role="columnheader"
+ scope="col">
+ E-mail address
+ </th>
+ <th
+ class="mdc-data-table__header-cell"
+ role="columnheader"
+ scope="col">
+ Scope
+ </th>
+ <th
+ class="mdc-data-table__header-cell"
+ role="columnheader"
+ scope="col">
+ </th>
+ </tr>
+ </thead>
+ <tbody class="mdc-data-table__content">
+ <tr class="mdc-data-table__row" v-for="user in users">
+ <td class="mdc-data-table__cell mdc-data-table__cell--numeric">{{ user.getId() }}</td>
+ <td class="mdc-data-table__cell">{{ user.getGoogleUid() || "-" }}</td>
+ <td class="mdc-data-table__cell">{{ user.getEmail() || "-" }}</td>
+ <td class="mdc-data-table__cell">{{ accessLevels[user.getAccessLevel()] }}</td>
+ <td class="mdc-data-table__cell">
+ <mcw-icon-button @click="(showUpdateForm(user))"><mcw-material-icon icon="edit" /></mcw-icon-button>
+ <mcw-icon-button @click="(showDeleteDialog(user))"><mcw-material-icon icon="delete" /></mcw-icon-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </mcw-data-table>
+ </div>
+ <mcw-fab @click="showAddForm()" class="add-user" icon="add" />
+ <mcw-dialog
+ v-model="deleteDialogOpen"
+ escape-key-action="close"
+ scrim-click-action="close"
+ aria-labelledby="delete-title"
+ aria-describedby="delete-content"
+ :auto-stack-buttons="true">
+ <mcw-dialog-title id="delete-title">Delete authorized user?</mcw-dialog-title>
+ <mcw-dialog-content id="delete-content">
+ <div>User {{ currentDeleteUser.getId() }} will no longer have access to the TW Power Tools dashboard.</div>
+ </mcw-dialog-content>
+ <mcw-dialog-footer>
+ <mcw-dialog-button action="dismiss">Cancel</mcw-dialog-button>
+ <mcw-dialog-button @click="deleteUser(currentDeleteUser)" action="accept">Delete</mcw-dialog-button>
+ </mcw-dialog-footer>
+ </mcw-dialog>
+ <!-- Add user dialog -->
+ <authorized-user-dialog @user-added="loadData()" v-model="addDialogOpen" />
+ <!-- Update user dialog -->
+ <authorized-user-dialog @user-updated="loadData()" v-model="updateDialogOpen" is-update :user="currentUpdateUser" />
+ </template>
+ <template v-else>
+ <not-authorized>
+ </not-authorized>
+ </template>
+</template>
+
+<style scoped>
+.container {
+ margin-top: 16px;
+ display: flex;
+ justify-content: space-evenly;
+}
+
+.add-user {
+ position: fixed;
+ right: 16px;
+ bottom: 16px;
+}
+</style>
diff --git a/frontend/src/pages/Home.vue b/frontend/src/pages/Home.vue
new file mode 100644
index 0000000..da5ff77
--- /dev/null
+++ b/frontend/src/pages/Home.vue
@@ -0,0 +1,17 @@
+<script>
+import Page from './utils/Page.vue';
+
+export default {
+ components: {
+ Page,
+ },
+};
+</script>
+
+<template>
+ <page>
+ <h1>TW Power Tools server</h1>
+ <p>Welcome to the TW Power Tools server dashboard. Currently the server is only used to manage <a href="https://docs.google.com/document/d/1O5YV6_WcxwrUyz-lwHOSTfZ3oyIFWj2EQee0VuKkhaA/edit" rel="noreferrer noopener">kill switches</a>.</p>
+ <p>While you have to sign in to enable/disable kill switches, you can see what's their status anonymously. Only some allowlisted users are currently able to do this. If you wish to be added to the allowlist, please <a href="https://iavm.xyz/b/twpowertools/new" rel="noreferrer noopener">fill in a bug</a> explaining why you're requiring access.</p>
+ </page>
+</template>
diff --git a/frontend/src/pages/KillSwitches.vue b/frontend/src/pages/KillSwitches.vue
new file mode 100644
index 0000000..e4fd9f6
--- /dev/null
+++ b/frontend/src/pages/KillSwitches.vue
@@ -0,0 +1,7 @@
+<script>
+export default {};
+</script>
+
+<template>
+ Kill switches!
+</template>
diff --git a/frontend/src/pages/NotAuthorized.vue b/frontend/src/pages/NotAuthorized.vue
new file mode 100644
index 0000000..f0b0a49
--- /dev/null
+++ b/frontend/src/pages/NotAuthorized.vue
@@ -0,0 +1,38 @@
+<script>
+import Page from './utils/Page.vue';
+
+export default {
+ components: {
+ Page,
+ },
+};
+</script>
+
+<template>
+ <page mini>
+ <div class="layout">
+ <mcw-material-icon class="main-icon" icon="no_accounts" />
+ <span class="label">You're not signed in.</span>
+ </div>
+ </page>
+</template>
+
+<style lang="scss" scoped>
+@use "@material/theme/color-palette" as palette;
+
+.layout {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 16px 0;
+}
+
+.main-icon {
+ font-size: 75px;
+ color: palette.$grey-800;
+}
+
+.label {
+ margin-top: 14px;
+}
+</style>
diff --git a/frontend/src/pages/utils/Page.vue b/frontend/src/pages/utils/Page.vue
new file mode 100644
index 0000000..16f0933
--- /dev/null
+++ b/frontend/src/pages/utils/Page.vue
@@ -0,0 +1,33 @@
+<script>
+export default {
+ props: {
+ 'mini': Boolean,
+ },
+};
+</script>
+
+<template>
+ <div class="page-content">
+ <div class="main mdc-elevation--z4" :class="{'main--mini': mini}">
+ <slot></slot>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.main {
+ position: relative;
+ display: block;
+ width: Calc(100% - 64px);
+ max-width: 1024px;
+ margin: 20px auto;
+ padding: 16px 16px 16px 16px;
+ border-radius: 2px;
+ background: #FFFFFF;
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+}
+
+.main.main--mini {
+ max-width: 400px;
+}
+</style>