1. Giriş
Google Workspace Eklentileri; Gmail, Dokümanlar, E-Tablolar ve Slaytlar gibi Google Workspace uygulamalarıyla entegre edilebilen özelleştirilmiş uygulamalardır. Geliştiricilerin, Google Workspace'e doğrudan entegre edilmiş özelleştirilmiş kullanıcı arayüzleri oluşturmasını sağlarlar. Eklentiler, kullanıcıların daha az bağlam değiştirerek daha verimli bir şekilde çalışmasına yardımcı olur.
Bu codelab'de Node.js, Cloud Run ve Datastore'u kullanarak basit bir görev listesi eklentisini nasıl oluşturacağınızı ve dağıtacağınızı öğreneceksiniz.
Neler öğreneceksiniz?
- Cloud Shell'i kullanma
- Cloud Run'a dağıt
- Eklenti dağıtımı tanımlayıcısı oluşturma ve dağıtma
- Kart çerçevesiyle Eklenti kullanıcı arayüzleri oluşturma
- Kullanıcı etkileşimlerine yanıt verme
- Eklentide kullanıcı bağlamından yararlanın
2. Kurulum ve şartlar
Kurulum talimatlarını uygulayarak bir Google Cloud projesi oluşturun ve eklentinin kullanacağı API'leri ve hizmetleri etkinleştirin.
Kendi hızınızda ortam kurulumu
- Cloud Console'u açın ve yeni proje oluşturun. (Gmail veya Google Workspace hesabınız yoksa hesap oluşturun.)
Tüm Google Cloud projelerinde benzersiz bir ad olan proje kimliğini unutmayın (yukarıdaki ad zaten alınmış ve size uygun olmayacaktır!). Bu kod laboratuvarın ilerleyen bölümlerinde PROJECT_ID
olarak adlandırılacaktır.
- Ardından, Google Cloud kaynaklarını kullanmak için Cloud Console'da faturalandırmayı etkinleştirin.
Bu codelab'i çalıştırmanın maliyeti, yüksek değildir. "Temizleme" bölümündeki talimatları izlediğinizden emin olun. bölümünü inceleyin. Yeni Google Cloud kullanıcıları 300 ABD doları ücretsiz deneme programından yararlanabilir.
Google Cloud Shell
Google Cloud dizüstü bilgisayarınızdan uzaktan çalıştırılabilse de bu codelab'de, Cloud'da çalışan bir komut satırı ortamı olan Google Cloud Shell'i kullanacağız.
Cloud Shell'i etkinleştirme
- Cloud Console'da, Cloud Shell'i etkinleştir simgesini tıklayın.
Cloud Shell'i ilk açtığınızda açıklayıcı bir karşılama mesajıyla karşılaşırsınız. Karşılama mesajını görürseniz Devam'ı tıklayın. Karşılama mesajı tekrar gösterilmez. Karşılama mesajı şöyle:
Temel hazırlık ve Cloud Shell'e bağlanmak yalnızca birkaç dakika sürer. Bağlandıktan sonra Cloud Shell Terminali gösterilir:
İhtiyaç duyduğunuz tüm geliştirme araçları bu sanal makinede yüklüdür. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud'da çalışarak ağ performansını ve kimlik doğrulamasını büyük ölçüde iyileştirir. Bu codelab'deki tüm çalışmalarınızı tarayıcı veya Chromebook'unuzla gerçekleştirebilirsiniz.
Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin proje kimliğinize ayarlandığını görürsünüz.
- Kimlik doğrulamanızın tamamlandığını onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud auth list
Cloud Shell'e GCP API çağrısı yapma yetkisi vermeniz istenirse Yetkilendir'i tıklayın.
Komut çıkışı
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
Etkin hesabı ayarlamak için şu komutu çalıştırın:
gcloud config set account <ACCOUNT>
Doğru projeyi seçtiğinizi onaylamak için Cloud Shell'de şu komutu çalıştırın:
gcloud config list project
Komut çıkışı
[core] project = <PROJECT_ID>
Doğru proje döndürülmezse şu komutla projeyi ayarlayabilirsiniz:
gcloud config set project <PROJECT_ID>
Komut çıkışı
Updated property [core/project].
Codelab'de, komut satırı işlemlerinin yanı sıra dosya düzenleme yöntemi de kullanılır. Dosya düzenleme için Cloud Shell araç çubuğunun sağ tarafındaki Düzenleyiciyi Aç düğmesini tıklayarak Cloud Shell'deki yerleşik kod düzenleyiciyi kullanabilirsiniz. Cloud Shell'de vim ve emacs gibi popüler düzenleyiciler de bulabilirsiniz.
3. Cloud Run, Datastore ve Eklenti API'lerini etkinleştirme
Cloud APIs'i etkinleştir
Cloud Shell'den, kullanılacak bileşenler için Cloud API'lerini etkinleştirin:
gcloud services enable \ run.googleapis.com \ cloudbuild.googleapis.com \ cloudresourcemanager.googleapis.com \ datastore.googleapis.com \ gsuiteaddons.googleapis.com
Bu işlemin tamamlanması birkaç dakika sürebilir.
İşlem tamamlandığında, şuna benzer bir başarı mesajı görüntülenir:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
Veri deposu örneği oluşturma
Ardından App Engine'i etkinleştirin ve Datastore veritabanı oluşturun. Datastore'u kullanmak için App Engine'i etkinleştirmek bir ön koşuldur. Ancak App Engine'i başka hiçbir amaçla kullanmayız.
gcloud app create --region=us-central gcloud firestore databases create --type=datastore-mode --region=us-central
OAuth izin ekranı oluşturma
Eklentinin çalışması ve verileri üzerinde işlem yapması için kullanıcı izni gerekir. Bunu etkinleştirmek için projenin izin ekranını yapılandırın. Codelab'de, başlamak için izin ekranını dahili bir uygulama olarak yapılandırmanız gerekir. Yani bu ekran herkese açık olarak dağıtılamaz.
- Google Cloud Console'u yeni bir sekmede veya pencerede açın.
- "Google Cloud Console"un yanında Aşağı oku tıklayıp projenizi seçin.
- Sol üst köşeden Menü'yü tıklayın.
- API'ler ve Hizmetler > Credentials (Kimlik Bilgisi). Projenizin kimlik bilgisi sayfası görüntülenir.
- OAuth izin ekranı'nı tıklayın. "OAuth izin ekranı" görünür.
- "Kullanıcı Türü"nün altında Dahili'yi seçin. Bir @gmail.com hesabı kullanıyorsanız Harici'yi seçin.
- Oluştur'u tıklayın. "Uygulama kaydını düzenle" görünür.
- Formu doldurun:
- Uygulama adı alanına "Yapılacaklar Listesi Eklentisi" yazın.
- Kullanıcı desteği e-postası alanına kişisel e-posta adresinizi girin.
- Geliştirici iletişim bilgileri bölümünün altına kişisel e-posta adresinizi girin.
- Kaydet ve Devam Et'i tıklayın. Bir Kapsamlar formu görüntülenir.
- Kapsamlar formunda Kaydet ve Devam Et'i tıklayın. Bir özet görüntülenir.
- Kontrol Paneline Dön'ü tıklayın.
4. İlk eklentiyi oluşturma
Projeyi ilk kullanıma hazırlama
Başlamak için basit bir "Merhaba dünya" ifadesi oluşturacaksınız. dağıtabilirsiniz. Eklentiler, https isteklerine yanıt veren ve kullanıcı arayüzünü ve yapılacak işlemleri açıklayan bir JSON yüküyle yanıt veren web hizmetleridir. Bu eklentide Node.js ve Express çerçevesini kullanacaksınız.
Bu şablon projesini oluşturmak için Cloud Shell'i kullanarak todo-add-on
adında yeni bir dizin oluşturun ve bu dizine gidin:
mkdir ~/todo-add-on cd ~/todo-add-on
Codelab ile ilgili tüm işlemleri bu dizinde yapacaksınız.
Node.js projesini başlatın:
npm init
AİOY, proje yapılandırması hakkında ad ve sürüm gibi çeşitli sorular sorar. Her soru için varsayılan değerleri kabul etmek üzere ENTER
tuşuna basın. Varsayılan giriş noktası, sonraki adımda oluşturacağımız index.js
adlı bir dosyadır.
Ardından Express web çerçevesini yükleyin:
npm install --save express express-async-handler
Eklenti arka ucunu oluşturma
Uygulamayı oluşturmaya başlamanın zamanı geldi.
index.js
adlı bir dosya oluşturun. Dosya oluşturmak için Cloud Shell penceresinin araç çubuğundaki Düzenleyiciyi Aç düğmesini tıklayarak Cloud Shell Düzenleyici'yi kullanabilirsiniz. Alternatif olarak vim veya emacs kullanarak Cloud Shell'deki dosyaları düzenleyebilir ve yönetebilirsiniz.
index.js
dosyasını oluşturduktan sonra aşağıdaki içeriği ekleyin:
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}`)
});
Sunucu, "Hello World"ü göstermekten başka bir şey yapmaz. Bunda herhangi bir sakınca yoktur. Daha sonra başka işlevler ekleyeceksiniz.
Cloud Run'a dağıt
Cloud Run'da dağıtmak için uygulamanın container mimarisine alınmış olması gerekir.
Kapsayıcıyı oluşturma
Aşağıdakileri içeren Dockerfile
adlı bir Dockerfile oluşturun:
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" ]
İstenmeyen dosyaları kapsayıcının dışında tutun
Kapsayıcının ışık tutmaya yardımcı olması için şunları içeren bir .dockerignore
dosyası oluşturun:
Dockerfile
.dockerignore
node_modules
npm-debug.log
Cloud Build'i etkinleştir
Bu codelab'de, yeni işlevler eklendikçe eklentiyi birkaç kez derleyip dağıtacaksınız. Container'ı derlemek için ayrı komutlar çalıştırmak, container kaydına aktarmak ve Cloud Build'e dağıtmak yerine Cloud Build'i kullanarak prosedürü düzenleyin. Uygulamanın nasıl derleneceği ve dağıtılacağı ile ilgili talimatları içeren bir cloudbuild.yaml
dosyası oluşturun:
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
Cloud Build'e uygulamayı dağıtma izni vermek için aşağıdaki komutları çalıştırın:
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
Eklenti arka ucunu oluşturma ve dağıtma
Derlemeyi başlatmak için Cloud Shell'de şu komutu çalıştırın:
gcloud builds submit
Tam derleme ve dağıtım işleminin tamamlanması, özellikle ilk seferde birkaç dakika sürebilir.
Derleme tamamlandığında hizmetin dağıtıldığını doğrulayın ve URL'yi bulun. Şu komutu çalıştırın:
gcloud run services list --platform managed
Bu URL'yi kopyalayın. Bir sonraki adımda (Google Workspace'e eklentiyi nasıl çağıracağını bildirme) ihtiyacınız olacak.
Eklentiyi kaydet
Sunucu çalışır durumda olduğuna göre, Google Workspace'in eklentiyi nasıl görüntüleyeceğini ve çağıracağını bilmesi için eklentiyi açıklayın.
Dağıtım açıklayıcısı oluşturma
Aşağıdaki içerikle deployment.json
dosyasını oluşturun. URL
yer tutucusu yerine dağıtılan uygulamanın URL'sini kullandığınızdan emin olun.
{
"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": {}
}
}
Şu komutu çalıştırarak dağıtım açıklayıcısını yükleyin:
gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json
Eklenti arka ucuna erişim yetkisi verme
Eklenti çerçevesinin hizmeti çağırmak için de izne ihtiyacı vardır. Aşağıdaki komutları çalıştırarak Google Workspace'in eklentiyi çağırmasına izin vermek için Cloud Run'ın IAM politikasını güncelleyin:
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"
Test için eklentiyi yükleyin
Hesabınız için eklentiyi geliştirme modunda yüklemek üzere Cloud Shell'de şu komutu çalıştırın:
gcloud workspace-add-ons deployments install todo-add-on
Yeni bir sekmede veya pencerede (Gmail)[https://mail.google.com/] sayfasını açın. Sağ tarafta onay işareti simgesi olan eklentiyi bulun.
Eklentiyi açmak için onay işareti simgesini tıklayın. Eklentiyi yetkilendirmek için bir istem gösterilir.
Erişimi Yetkilendir'i tıklayın ve pop-up'taki yetkilendirme akışı talimatlarını uygulayın. İşlem tamamlandığında eklenti otomatik olarak yeniden yüklenir ve "Hello world!" (Merhaba dünya!) mesajını alırsınız.
Tebrikler! Artık dağıtılıp yüklenmiş basit bir eklentiniz var. Şimdi onu bir görev listesi uygulamasına dönüştürme zamanı.
5. Kullanıcı kimliğine erişme
Eklentiler genellikle birçok kullanıcı tarafından kendilerine veya kuruluşlarına özel bilgilerle çalışmak için kullanılır. Bu codelab'de eklenti yalnızca geçerli kullanıcının görevlerini göstermelidir. Kullanıcı kimliği, kodunun çözülmesi gereken bir kimlik jetonu aracılığıyla eklentiye gönderilir.
Dağıtım tanımlayıcısına kapsam ekleyin
Kullanıcı kimliği varsayılan olarak gönderilmez. Bu veriler kullanıcı verilerini içerir ve eklentinin bu verilere erişmek için izne ihtiyacı vardır. Bu izni elde etmek için deployment.json
uygulamasını güncelleyin ve openid
ve email
OAuth kapsamlarını, eklentinin gerektirdiği kapsamlar listesine ekleyin. OAuth kapsamları eklendikten sonra, eklenti, eklentiyi bir sonraki kullanışlarında kullanıcılardan erişim izni ister.
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"openid",
"email"
],
Ardından, Cloud Shell'de dağıtım açıklayıcıyı güncellemek için şu komutu çalıştırın:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Eklenti sunucusunu güncelleme
Eklenti, kullanıcı kimliğini isteyecek şekilde yapılandırılmış olsa da uygulamanın güncellenmesi gerekir.
Kimlik jetonunu ayrıştır
İlk olarak Google kimlik doğrulama kitaplığını projeye ekleyin:
npm install --save google-auth-library
Ardından OAuth2Client
yönergesini zorunlu kılacak şekilde index.js
öğesini düzenleyin:
const { OAuth2Client } = require('google-auth-library');
Ardından, kimlik jetonunu ayrıştırmak için bir yardımcı yöntem ekleyin:
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
Kullanıcı kimliğini göster
Tüm görev listesi işlevlerini eklemeden önce kontrol noktası belirlemek için iyi bir zamandır. "Merhaba dünya" yerine kullanıcının e-posta adresini ve benzersiz kimliğini yazdırmak için uygulamanın yolunu güncelleyin.
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);
}));
Bu değişikliklerden sonra elde edilen index.js
dosyası aşağıdaki gibi görünmelidir:
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}`)
});
Yeniden dağıt ve test et
Eklentiyi yeniden derleyip yeniden dağıtın. Cloud Shell'den şu komutu çalıştırın:
gcloud builds submit
Sunucu yeniden dağıtıldıktan sonra Gmail'i açın veya yeniden yükleyin ve eklentiyi tekrar açın. Kapsamlar değiştiğinden eklenti yeniden yetkilendirme ister. Eklentiyi tekrar yetkilendirin. İşlem tamamlandığında eklenti e-posta adresinizi ve kullanıcı kimliğinizi gösterir.
Eklenti kullanıcının kim olduğunu bildiğine göre görev listesi işlevini eklemeye başlayabilirsiniz.
6. Görev listesini uygulama
Codelab için ilk veri modeli oldukça basittir: Her biri görev açıklayıcı metnine ve bir zaman damgasına sahip Task
varlıklarından oluşan bir liste.
Veri deposu dizinini oluşturma
Datastore, proje için codelab'in önceki bölümlerinde daha önce etkinleştirilmişti. Bu özellik, şema gerektirmez ancak bileşik sorgular için açıkça dizin oluşturmayı gerektirir. Dizinin oluşturulması birkaç dakika sürebilir; bu nedenle önce bu işlemi yapabilirsiniz.
Aşağıdakileri içeren index.yaml
adlı bir dosya oluşturun:
indexes:
- kind: Task
ancestor: yes
properties:
- name: created
Ardından Datastore dizinlerini güncelleyin:
gcloud datastore indexes create index.yaml
Devam etmeniz istendiğinde klavyenizdeki ENTER tuşuna basın. Dizin oluşturma işlemi arka planda gerçekleşir. Bu işlem gerçekleşirken, "yapılacaklar"ı uygulamak için eklenti kodunu güncellemeye başlayın.
Eklenti arka ucunu güncelleme
Datastore kitaplığını projeye yükleyin:
npm install --save @google-cloud/datastore
Datastore'a okuma ve yazma
"Yapılacaklar"ı uygulamak için index.js
uygulamasını güncelleyin İlk olarak veri deposu kitaplığını içe aktarın ve istemciyi oluşturun:
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
Datastore'dan görevleri okumak ve yazmak için yöntemler ekleyin:
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);
}
Kullanıcı arayüzü oluşturmayı uygulama
Değişikliklerin çoğu eklenti kullanıcı arayüzünde yapılmıştır. Daha önce, kullanıcı arayüzü tarafından döndürülen tüm kartlar statikti. Bunlar, mevcut verilere bağlı olarak değiştirilmiyordu. Burada, kartın kullanıcının mevcut görev listesine göre dinamik olarak oluşturulması gerekir.
Codelab'in kullanıcı arayüzünde bir metin girişi ve görevlerin tamamlandı olarak işaretlendiği onay kutuları bulunan bir görev listesi yer alır. Bunların her biri, kullanıcı bir görev eklediğinde veya sildiğinde eklenti sunucusuna geri çağırmayla sonuçlanan bir onChangeAction
özelliğine sahiptir. Bu durumların her birinde kullanıcı arayüzünün, güncellenen görev listesiyle yeniden oluşturulması gerekir. Şimdi, kart kullanıcı arayüzünü oluşturmak için yeni bir yöntem sunuyoruz.
index.js
öğesini düzenlemeye devam edin ve aşağıdaki yöntemi ekleyin:
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;
}
Rotaları güncelle
Artık Datastore'da okuma, yazma ve kullanıcı arayüzünü derlemek için yardımcı yöntemler olduğuna göre bunları uygulama rotalarında birbirine bağlayabiliriz. Mevcut yolu değiştirip iki rota daha ekleyin: bunlardan biri görev eklemek, diğeri ise silmek içindir.
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);
}));
Tamamen işlevsel index.js
dosyasının son hali aşağıda verilmiştir:
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}`)
});
Yeniden dağıt ve test et
Eklentiyi yeniden derleyip dağıtmak için bir derleme başlatın. Cloud Shell'de şu komutu çalıştırın:
gcloud builds submit
Gmail'de eklentiyi yeniden yüklediğinizde yeni kullanıcı arayüzü görünür. Bir dakikanızı ayırarak eklentiyi keşfedin. Girişe metin girip klavyenizdeki ENTER tuşuna basarak birkaç görev ekleyin. Ardından, görevleri silmek için onay kutusunu işaretleyin.
İsterseniz bu codelab'deki son adıma atlayıp projenizi temizleyebilirsiniz. Eklentiler hakkında daha fazla bilgi edinmeye devam etmek istiyorsanız uygulayabileceğiniz bir adım daha vardır.
7. (İsteğe bağlı) Bağlam ekleme
Eklentilerin en güçlü özelliklerinden biri bağlam farkındalığıdır. Eklentiler, kullanıcı izniyle, kullanıcının baktığı e-posta, takvim etkinliği ve doküman gibi Google Workspace bağlamlarına erişebilir. Eklentiler, içerik ekleme gibi işlemler de yapabilir. Bu codelab'de, Workspace düzenleyicileri (Dokümanlar, E-Tablolar ve Slaytlar) için bağlam desteği ekleyerek mevcut dokümanı düzenleyicilerde oluşturulan görevlere ekleyeceksiniz. Görev görüntülendiğinde, tıklandığında doküman yeni bir sekmede açılır. Böylece kullanıcının görevini tamamlaması için doküman yeni bir sekmede açılır.
Eklenti arka ucunu güncelleme
newTask
rotasını güncelle
İlk olarak, /newTask
rotasını, varsa bir göreve doküman kimliğini dahil edecek şekilde güncelleyin:
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);
}));
Yeni oluşturulan görevlere artık geçerli doküman kimliği eklendi. Ancak düzenleyicilerdeki bağlam varsayılan olarak paylaşılmaz. Diğer kullanıcı verilerinde olduğu gibi, kullanıcının eklentinin verilere erişmesine izin vermesi gerekir. Bilgilerin aşırı paylaşılmasını önlemek için tercih edilen yaklaşım, her dosya için ayrı izin talep etmek ve izin vermektir.
Kullanıcı arayüzünü güncelleme
index.js
uygulamasında, iki değişiklik yapmak için buildCard
uygulamasını güncelleyin. Birincisi, görevlerin oluşturulmasını, varsa dokümanın bağlantısını içerecek şekilde güncellemek. İkincisi ise eklenti bir düzenleyicide oluşturulduysa ve dosya erişimi henüz verilmemişse isteğe bağlı bir yetkilendirme istemi görüntülemektir.
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;
}
Dosya yetkilendirme yolunu uygulama
Yetkilendirme düğmesi, uygulamaya yeni bir rota ekler. Bu nedenle, bunu uygulamaya geçirelim. Bu rotada yeni bir kavram olan ana makine uygulama işlemleri tanıtılmaktadır. Bunlar, eklentinin ana makine uygulamasıyla etkileşim kurmaya yönelik özel talimatlardır. Bu durumda, mevcut düzenleyici dosyasına erişim isteyebilirsiniz.
index.js
içinde /authorizeFile
rotasını ekleyin:
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
Tamamen işlevsel index.js
dosyasının son hali aşağıda verilmiştir:
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}`)
});
Dağıtım tanımlayıcısına kapsam ekleyin
Sunucuyu yeniden oluşturmadan önce, eklenti dağıtım açıklayıcısını https://www.googleapis.com/auth/drive.file
OAuth kapsamını içerecek şekilde güncelleyin. https://www.googleapis.com/auth/drive.file
kapsamlarını, OAuth kapsamları listesine eklemek için deployment.json
öğesini güncelleyin:
"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"
]
Şu Cloud Shell komutunu çalıştırarak yeni sürümü yükleyin:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Yeniden dağıt ve test et
Son olarak, sunucuyu yeniden oluşturun. Cloud Shell'den şu komutu çalıştırın:
gcloud builds submit
İşlem tamamlandığında, Gmail'i açmak yerine mevcut bir Google dokümanını açın veya doc.new dosyasını açarak yeni bir doküman oluşturun. Yeni bir doküman oluşturuyorsanız metin girdiğinizden veya dosyaya bir ad verdiğinizden emin olun.
Eklentiyi açın. Eklentinin altında, Dosya Erişimini Yetkilendir düğmesi gösterilir. Düğmeyi tıklayın, ardından dosyaya erişim izni verin.
Yetki verildiğinde, düzenleyicideyken bir görev ekleyin. Görevde, dokümanın eklendiğini belirten bir etiket bulunur. Bağlantıyı tıkladığınızda doküman yeni bir sekmede açılır. Elbette, açık olan dokümanı açmak biraz saçma olabilir. Kullanıcı arayüzünü, geçerli dokümana ilişkin bağlantıları filtrelemek için optimize etmek istiyorsanız, bu ekstra krediyi kullanabilirsiniz.
8. Tebrikler
Tebrikler! Cloud Run'ı kullanarak başarıyla bir Google Workspace eklentisi derleyip dağıttınız. Codelab, eklenti oluşturmayla ilgili temel kavramların çoğunu kapsasa da keşfedilecek daha pek çok konu bulunuyor. Aşağıdaki kaynaklara bakın ve ek ücretlerden kaçınmak için projenizi temizlemeyi unutmayın.
Temizleme
Eklentiyi hesabınızdan kaldırmak için Cloud Shell'de şu komutu çalıştırın:
gcloud workspace-add-ons deployments uninstall todo-add-on
Bu eğiticide kullanılan kaynaklar için Google Cloud Platform hesabınızın ücretlendirilmesini önlemek amacıyla:
- Cloud Console'da Kaynakları yönetin sayfasına gidin. Sol üst köşeden Menü 'yü tıklayın > IAM ve Yönetici > Kaynakları Yönetin.
- Proje listesinde projenizi seçin ve Sil'i tıklayın.
- İletişim kutusuna proje kimliğini yazın ve projeyi silmek için Kapat'ı tıklayın.
Daha fazla bilgi
- Google Workspace Eklentilerine genel bakış
- Marketplace'teki mevcut uygulamaları ve eklentileri bulma