1. Pengantar
Add-on Google Workspace adalah aplikasi yang disesuaikan yang terintegrasi dengan aplikasi Google Workspace, seperti Gmail, Dokumen, Spreadsheet, dan Slide. Keduanya memungkinkan developer membuat antarmuka pengguna khusus yang terintegrasi langsung ke Google Workspace. Add-on membantu pengguna bekerja secara lebih efisien dengan lebih sedikit pengalihan konteks.
Dalam codelab ini, Anda akan mempelajari cara membangun dan men-deploy add-on daftar tugas sederhana menggunakan Node.js, Cloud Run, dan Datastore.
Yang akan Anda pelajari
- Menggunakan Cloud Shell
- Men-deploy ke Cloud Run
- Membuat dan men-deploy deskriptor deployment Add-on
- Membuat UI Add-on dengan framework kartu
- Merespons interaksi pengguna
- Memanfaatkan konteks pengguna dalam Add-on
2. Penyiapan dan persyaratan
Ikuti petunjuk penyiapan untuk membuat project Google Cloud serta mengaktifkan API dan layanan yang akan digunakan add-on.
Penyiapan lingkungan mandiri
- Buka Konsol Cloud dan buat project baru. (Jika belum memiliki akun Gmail atau Google Workspace, buat akun.)
Ingat project ID, nama unik di semua project Google Cloud (maaf, nama di atas telah digunakan dan tidak akan berfungsi untuk Anda!) Project ID tersebut selanjutnya akan dirujuk di codelab ini sebagai PROJECT_ID
.
- Selanjutnya, untuk menggunakan resource Google Cloud, aktifkan penagihan di Konsol Cloud.
Menjalankan operasi dalam codelab ini seharusnya tidak memerlukan banyak biaya, bahkan mungkin tidak sama sekali. Pastikan untuk mengikuti petunjuk di bagian "Pembersihan" di akhir codelab yang memberi tahu Anda cara mematikan resource agar Anda tidak dikenai biaya setelah mengikuti tutorial ini. Pengguna baru Google Cloud memenuhi syarat untuk mengikuti program Uji Coba Gratis senilai $300 USD.
Google Cloud Shell
Meskipun Google Cloud dapat dioperasikan secara jarak jauh dari laptop Anda, dalam codelab ini kita akan menggunakan Google Cloud Shell, yakni lingkungan command line yang berjalan di Cloud.
Mengaktifkan Cloud Shell
- Dari Cloud Console, klik Aktifkan Cloud Shell .
Saat pertama kali membuka Cloud Shell, Anda akan melihat pesan selamat datang yang bersifat deskriptif. Jika Anda melihat pesan selamat datang, klik Continue. Pesan selamat datang tidak akan muncul lagi. Berikut adalah pesan selamat datang:
Perlu waktu beberapa saat untuk penyediaan dan terhubung ke Cloud Shell. Setelah terhubung, Anda akan melihat Terminal Cloud Shell:
Mesin virtual ini dimuat dengan semua alat pengembangan yang Anda butuhkan. Layanan ini menawarkan direktori beranda tetap sebesar 5 GB dan beroperasi di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Semua pekerjaan Anda dalam codelab ini dapat dilakukan dengan browser atau Chromebook.
Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda sudah diautentikasi dan project sudah ditetapkan ke project ID Anda.
- Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
gcloud auth list
Jika Anda diminta untuk mengotorisasi Cloud Shell agar dapat melakukan panggilan API GCP, klik Authorize.
Output perintah
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
Untuk menetapkan akun aktif, jalankan:
gcloud config set account <ACCOUNT>
Untuk mengonfirmasi bahwa Anda telah memilih project yang benar, di Cloud Shell, jalankan:
gcloud config list project
Output perintah
[core] project = <PROJECT_ID>
Jika project yang benar tidak dihasilkan, Anda dapat menyetelnya dengan perintah ini:
gcloud config set project <PROJECT_ID>
Output perintah
Updated property [core/project].
Codelab ini menggunakan gabungan operasi command line serta pengeditan file. Untuk pengeditan file, Anda dapat menggunakan editor kode bawaan di Cloud Shell dengan mengklik tombol Open Editor di sebelah kanan toolbar Cloud Shell. Anda juga akan menemukan editor populer, seperti vim dan emacs, yang tersedia di Cloud Shell.
3. Mengaktifkan Cloud Run, Datastore, dan API Add-on
Aktifkan Cloud API
Dari Cloud Shell, aktifkan Cloud API untuk komponen yang akan digunakan:
gcloud services enable \ run.googleapis.com \ cloudbuild.googleapis.com \ cloudresourcemanager.googleapis.com \ datastore.googleapis.com \ gsuiteaddons.googleapis.com
Operasi ini mungkin memerlukan waktu beberapa saat sampai selesai.
Setelah selesai, akan muncul pesan sukses yang mirip dengan yang berikut ini:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
Membuat instance datastore
Selanjutnya, aktifkan App Engine dan buat database Datastore. Mengaktifkan App Engine adalah prasyarat untuk menggunakan Datastore, tetapi kita tidak akan menggunakan App Engine untuk hal lainnya.
gcloud app create --region=us-central gcloud firestore databases create --type=datastore-mode --region=us-central
Membuat layar izin OAuth
Add-on memerlukan izin pengguna untuk menjalankan dan mengambil tindakan terhadap data mereka. Konfigurasi layar izin project untuk mengaktifkannya. Untuk codelab, Anda akan mengonfigurasi layar izin sebagai aplikasi internal. Artinya, memulai penggunaan bukan untuk distribusi publik.
- Buka Konsol Google Cloud di tab atau jendela baru.
- Di samping "Konsol Google Cloud", Klik Panah bawah , lalu pilih project Anda.
- Di pojok kiri atas, klik Menu .
- Klik API & Layanan > Kredensial. Halaman kredensial untuk project Anda akan muncul.
- Klik OAuth consent screen. "Layar izin OAuth" muncul.
- Di bagian "User Type", pilih Internal. Jika menggunakan akun @gmail.com, pilih Eksternal.
- Klik Buat. Opsi "Edit pendaftaran aplikasi" akan muncul.
- Isi formulirnya:
- Di App name, masukkan "Todo Add-on".
- Di bagian Email dukungan pengguna, masukkan alamat email pribadi Anda.
- Di bagian Informasi kontak developer, masukkan alamat email pribadi Anda.
- Klik Simpan dan Lanjutkan. Formulir Cakupan akan muncul.
- Dari formulir Cakupan, klik Simpan dan Lanjutkan. Ringkasan akan muncul.
- Klik Kembali ke Dasbor.
4. Membuat add-on awal
Menginisialisasi project
Untuk memulai, Anda akan membuat "Halo dunia" sederhana add-on ini dan men-deploy-nya. Add-on adalah layanan web yang merespons permintaan https dan merespons dengan payload JSON yang mendeskripsikan UI dan tindakan yang harus diambil. Dalam add-on ini, Anda akan menggunakan Node.js dan framework Express.
Untuk membuat project template ini, gunakan Cloud Shell untuk membuat direktori baru bernama todo-add-on
lalu buka direktori tersebut:
mkdir ~/todo-add-on cd ~/todo-add-on
Anda akan melakukan semua pekerjaan untuk codelab di direktori ini.
Inisialisasi project Node.js:
npm init
NPM akan mengajukan beberapa pertanyaan tentang konfigurasi project, seperti nama dan versi. Untuk setiap pertanyaan, tekan ENTER
untuk menyetujui nilai default. Titik entri default adalah file bernama index.js
, yang akan kita buat berikutnya.
Selanjutnya, instal framework web Express:
npm install --save express express-async-handler
Membuat backend add-on
Saatnya untuk mulai membuat aplikasi.
Buat file bernama index.js
. Untuk membuat file, Anda dapat menggunakan Cloud Shell Editor dengan mengklik tombol Open Editor di toolbar jendela Cloud Shell. Atau, Anda dapat mengedit dan mengelola file di Cloud Shell menggunakan vim atau emacs.
Setelah membuat file index.js
, tambahkan konten berikut:
const express = require('express');
const asyncHandler = require('express-async-handler');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post("/", asyncHandler(async (req, res) => {
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello world!`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
Server tidak melakukan banyak hal selain menampilkan ‘Hello world’ pesan dan itu tidak masalah. Anda akan menambahkan lebih banyak fungsi nanti.
Men-deploy ke Cloud Run
Untuk men-deploy di Cloud Run, aplikasi harus di dalam container.
Membuat penampung
Buat Dockerfile bernama Dockerfile
yang berisi:
FROM node:12-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Run the web service on container startup.
CMD [ "node", "index.js" ]
Mengeluarkan file yang tidak diinginkan dari penampung
Untuk membantu menjaga lampu penampung tetap ringan, buat file .dockerignore
yang berisi:
Dockerfile
.dockerignore
node_modules
npm-debug.log
Mengaktifkan Cloud Build
Dalam codelab ini, Anda akan membangun dan men-deploy add-on beberapa kali saat fungsi baru ditambahkan. Daripada menjalankan perintah terpisah untuk membangun container, mengirimnya ke register container, lalu men-deploy-nya ke Cloud Build, gunakan Cloud Build untuk mengorkestrasi prosedurnya. Buat file cloudbuild.yaml
yang berisi petunjuk cara membangun dan men-deploy aplikasi:
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME', '.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME']
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- '$_SERVICE_NAME'
- '--image'
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
- '--region'
- '$_REGION'
- '--platform'
- 'managed'
images:
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
substitutions:
_SERVICE_NAME: todo-add-on
_REGION: us-central1
Jalankan perintah berikut untuk memberi Cloud Build izin guna men-deploy aplikasi:
PROJECT_ID=$(gcloud config list --format='value(core.project)') PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') gcloud projects add-iam-policy-binding $PROJECT_ID \ --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ --role=roles/run.admin gcloud iam service-accounts add-iam-policy-binding \ $PROJECT_NUMBER-compute@developer.gserviceaccount.com \ --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ --role=roles/iam.serviceAccountUser
Membangun dan men-deploy backend add-on
Untuk memulai build, di Cloud Shell, jalankan:
gcloud builds submit
Proses build dan deploy secara penuh mungkin memerlukan waktu beberapa menit, terutama saat pertama kali dijalankan.
Setelah build selesai, pastikan layanan telah di-deploy dan temukan URL-nya. Jalankan perintah:
gcloud run services list --platform managed
Salin URL ini. Anda akan memerlukannya untuk langkah berikutnya – memberi tahu Google Workspace cara memanggil add-on.
Mendaftarkan add-on
Setelah server aktif dan berjalan, jelaskan add-on ini agar Google Workspace mengetahui cara menampilkan dan memanggil add-on.
Membuat deskriptor deployment
Buat file deployment.json
dengan konten berikut. Pastikan untuk menggunakan URL aplikasi yang di-deploy sebagai pengganti placeholder URL
.
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute"
],
"addOns": {
"common": {
"name": "Todo Codelab",
"logoUrl": "https://raw.githubusercontent.com/webdog/octicons-png/main/black/check.png",
"homepageTrigger": {
"runFunction": "URL"
}
},
"gmail": {},
"drive": {},
"calendar": {},
"docs": {},
"sheets": {},
"slides": {}
}
}
Upload deskriptor deployment dengan menjalankan perintah:
gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json
Mengizinkan akses ke backend add-on
Framework add-on juga memerlukan izin untuk memanggil layanan. Jalankan perintah berikut untuk memperbarui kebijakan IAM untuk Cloud Run agar Google Workspace dapat memanggil add-on:
SERVICE_ACCOUNT_EMAIL=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)")
gcloud run services add-iam-policy-binding todo-add-on --platform managed --region us-central1 --role roles/run.invoker --member "serviceAccount:$SERVICE_ACCOUNT_EMAIL"
Instal add-on untuk pengujian
Untuk menginstal add-on dalam mode pengembangan untuk akun Anda, jalankan di Cloud Shell:
gcloud workspace-add-ons deployments install todo-add-on
Buka (Gmail)[https://mail.google.com/] di tab atau jendela baru. Di sisi kanan, temukan add-on dengan ikon tanda centang.
Untuk membuka add-on, klik ikon tanda centang. Permintaan untuk mengizinkan add-on akan muncul.
Klik Otorisasi Akses dan ikuti petunjuk alur otorisasi di pop-up. Setelah selesai, add-on akan otomatis dimuat ulang dan menampilkan pesan 'Halo dunia!' untuk membuat pesan email baru.
Selamat! Sekarang Anda telah memiliki add-on sederhana yang di-deploy dan diinstal. Saatnya mengubahnya menjadi aplikasi daftar tugas!
5. Mengakses identitas pengguna
Add-on biasanya digunakan oleh banyak pengguna untuk menangani informasi yang bersifat pribadi bagi mereka atau organisasi mereka. Dalam codelab ini, add-on hanya akan menampilkan tugas untuk pengguna saat ini. Identitas pengguna dikirim ke add-on melalui token identitas yang perlu didekode.
Menambahkan cakupan ke deskriptor deployment
Identitas pengguna tidak dikirim secara default. Data pengguna adalah data pengguna dan add-on memerlukan izin untuk mengaksesnya. Untuk mendapatkan izin tersebut, update deployment.json
dan tambahkan cakupan OAuth openid
dan email
ke daftar cakupan yang diperlukan add-on. Setelah menambahkan cakupan OAuth, add-on akan meminta pengguna untuk memberikan akses saat mereka menggunakan add-on lagi.
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"openid",
"email"
],
Kemudian, di Cloud Shell, jalankan perintah ini untuk memperbarui deskriptor deployment:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Mengupdate server add-on
Meskipun add-on dikonfigurasi untuk meminta identitas pengguna, penerapan masih perlu diperbarui.
Mengurai token identitas
Mulailah dengan menambahkan library autentikasi Google ke project:
npm install --save google-auth-library
Kemudian, edit index.js
untuk mewajibkan OAuth2Client
:
const { OAuth2Client } = require('google-auth-library');
Kemudian tambahkan metode helper untuk mengurai token ID:
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
Menampilkan identitas pengguna
Ini adalah waktu yang tepat untuk checkpoint sebelum menambahkan semua fungsi daftar tugas. Perbarui rute aplikasi untuk mencetak alamat email dan ID unik pengguna, bukan 'Halo dunia'.
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
Setelah perubahan ini, file index.js
yang dihasilkan akan terlihat seperti ini:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
Men-deploy ulang dan menguji
Buat ulang dan deploy ulang add-on. Dari Cloud Shell, jalankan:
gcloud builds submit
Setelah server di-deploy ulang, buka atau muat ulang Gmail, lalu buka lagi add-on tersebut. Karena cakupannya telah berubah, add-on akan meminta otorisasi ulang. Izinkan add-on lagi, dan setelah selesai add-on akan menampilkan alamat email dan ID pengguna Anda.
Setelah add-on mengetahui siapa penggunanya, Anda dapat mulai menambahkan fungsi daftar tugas.
6. Mengimplementasikan daftar tugas
Model data awal untuk codelab ini sangat sederhana: daftar entity Task
, masing-masing dengan properti untuk teks deskriptif tugas dan stempel waktu.
Membuat indeks datastore
Datastore sudah diaktifkan untuk project sebelumnya di codelab. Class ini tidak memerlukan skema, meskipun memerlukan pembuatan indeks secara eksplisit untuk kueri gabungan. Membuat indeks dapat memerlukan waktu beberapa menit, jadi Anda harus melakukannya terlebih dahulu.
Buat file bernama index.yaml
dengan hal berikut:
indexes:
- kind: Task
ancestor: yes
properties:
- name: created
Kemudian update indeks Datastore:
gcloud datastore indexes create index.yaml
Jika diminta untuk melanjutkan, tekan ENTER di keyboard. Pembuatan indeks terjadi di latar belakang. Selama proses ini berlangsung, mulailah mengupdate kode add-on untuk menerapkan "daftar tugas".
Mengupdate backend add-on
Instal library Datastore ke project:
npm install --save @google-cloud/datastore
Membaca dan menulis ke Datastore
Update index.js
untuk mengimplementasikan "daftar tugas" dimulai dengan mengimpor library datastore dan membuat klien:
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
Tambahkan metode untuk membaca dan menulis tugas dari Datastore:
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
Mengimplementasikan rendering UI
Sebagian besar perubahan terjadi pada UI add-on. Sebelumnya, semua kartu yang ditampilkan oleh UI bersifat statis, dan tidak berubah bergantung pada data yang tersedia. Di sini, kartu perlu dibuat secara dinamis berdasarkan daftar tugas pengguna saat ini.
UI untuk codelab terdiri dari input teks beserta daftar tugas dengan kotak centang untuk menandainya sebagai selesai. Masing-masing juga memiliki properti onChangeAction
yang menghasilkan callback ke server add-on saat pengguna menambahkan atau menghapus tugas. Dalam setiap kasus ini, UI harus dirender ulang dengan daftar tugas yang diperbarui. Untuk menangani hal ini, kami akan memperkenalkan metode baru untuk membangun UI kartu.
Lanjutkan mengedit index.js
dan tambahkan metode berikut:
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
Memperbarui rute
Setelah memiliki metode bantuan untuk membaca dan menulis ke Datastore dan membangun UI, mari kita menghubungkannya dalam rute aplikasi. Ganti rute yang ada dan tambahkan dua rute lainnya: satu untuk menambahkan tugas dan satu lagi untuk menghapusnya.
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
Berikut file index.js
final yang berfungsi penuh:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
Men-deploy ulang dan menguji
Untuk membangun ulang dan men-deploy ulang add-on, mulai build. Di Cloud Shell, jalankan:
gcloud builds submit
Di Gmail, muat ulang add-on dan UI baru akan muncul. Luangkan waktu sejenak untuk menjelajahi add-on ini. Tambahkan beberapa tugas dengan memasukkan beberapa teks ke dalam input dan menekan ENTER pada {i>keyboard<i} Anda, lalu klik kotak centang untuk menghapusnya.
Jika mau, Anda dapat langsung melanjutkan ke langkah terakhir dalam codelab ini dan menyelesaikan project. Atau, jika Anda ingin terus mempelajari add-on lebih lanjut, ada satu langkah lagi yang dapat Anda selesaikan.
7. (Opsional) Menambahkan konteks
Salah satu fitur add-on yang paling canggih adalah kemampuan kontekstual. Dengan izin pengguna, add-on dapat mengakses konteks Google Workspace seperti email yang dilihat pengguna, acara kalender, dan dokumen. Add-on juga dapat mengambil tindakan seperti menyisipkan konten. Dalam codelab ini, Anda akan menambahkan dukungan konteks untuk editor Workspace (Dokumen, Spreadsheet, dan Slide) untuk melampirkan dokumen saat ini ke tugas apa pun yang dibuat saat berada di editor. Saat tugas ditampilkan, mengkliknya akan membuka dokumen di tab baru untuk membawa pengguna kembali ke dokumen guna menyelesaikan tugasnya.
Mengupdate backend add-on
Perbarui rute newTask
Pertama, perbarui rute /newTask
untuk menyertakan ID dokumen dalam tugas jika tersedia:
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
Tugas yang baru dibuat kini menyertakan ID dokumen saat ini. Namun, konteks di editor tidak dibagikan secara default. Seperti data pengguna lainnya, pengguna harus memberikan izin agar add-on dapat mengakses data. Untuk mencegah pembagian informasi yang berlebihan, pendekatan yang lebih disukai adalah meminta dan memberikan izin per file.
Mengupdate UI
Di index.js
, update buildCard
untuk membuat dua perubahan. Yang pertama adalah memperbarui rendering tugas untuk menyertakan tautan ke dokumen jika ada. Yang kedua adalah menampilkan permintaan otorisasi opsional jika add-on dirender di editor dan akses file belum diberikan.
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
Menerapkan rute otorisasi file
Tombol otorisasi menambahkan rute baru ke aplikasi, jadi mari kita implementasikan. Rute ini memperkenalkan konsep baru, yaitu tindakan aplikasi host. Ini adalah petunjuk khusus untuk berinteraksi dengan aplikasi host add-on. Dalam kasus ini, minta akses ke file editor saat ini.
Di index.js
, tambahkan rute /authorizeFile
:
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
Berikut file index.js
final yang berfungsi penuh:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
Menambahkan cakupan ke deskriptor deployment
Sebelum membangun ulang server, perbarui deskriptor deployment add-on untuk menyertakan cakupan OAuth https://www.googleapis.com/auth/drive.file
. Update deployment.json
untuk menambahkan https://www.googleapis.com/auth/drive.file
ke daftar cakupan OAuth:
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"https://www.googleapis.com/auth/drive.file",
"openid",
"email"
]
Upload versi baru dengan menjalankan perintah Cloud Shell ini:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Men-deploy ulang dan menguji
Terakhir, bangun ulang server. Dari Cloud Shell, jalankan:
gcloud builds submit
Setelah selesai, buka dokumen Google yang sudah ada atau buat dokumen baru dengan membuka doc.new, bukan membuka Gmail. Jika membuat dokumen baru, pastikan untuk memasukkan teks atau memberi nama file.
Buka add-on. Add-on menampilkan tombol Authorize File Access di bagian bawah add-on. Klik tombol, lalu izinkan akses ke file.
Setelah diberi otorisasi, tambahkan tugas saat berada di editor. Tugas tersebut memiliki label yang menunjukkan bahwa dokumen terlampir. Mengklik link akan membuka dokumen di tab baru. Tentu saja, membuka dokumen yang sudah Anda buka agak konyol. Jika Anda ingin mengoptimalkan UI untuk memfilter link untuk dokumen saat ini, pertimbangkan kredit tambahan tersebut.
8. Selamat
Selamat! Anda telah berhasil membangun dan men-deploy Add-on Google Workpace menggunakan Cloud Run. Meskipun codelab ini membahas banyak konsep inti untuk membangun add-on, masih banyak lagi hal yang dapat dipelajari. Baca referensi di bawah dan jangan lupa untuk menghapus project Anda agar terhindar dari biaya tambahan.
Pembersihan
Untuk meng-uninstal add-on dari akun Anda, jalankan perintah ini di Cloud Shell:
gcloud workspace-add-ons deployments uninstall todo-add-on
Agar tidak menimbulkan biaya pada akun Google Cloud Platform Anda untuk resource yang digunakan dalam tutorial ini:
- Di Cloud Console, buka halaman Manage resource. Di pojok kiri atas, klik Menu > IAM & Admin > Manage Resources.
- Dalam daftar project, pilih project Anda lalu klik Delete.
- Pada dialog, ketik project ID, lalu klik Shut down untuk menghapus project.
Pelajari lebih lanjut
- Ringkasan Add-on Google Workspace
- Temukan aplikasi dan add-on yang ada di marketplace