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>