Google 表格、Excel 和 Airtable
Diagrams 中,并将图表链接到该服务中的地址。如何组合在一起
提供商调用服务的 API 并返回图表数据。该文档存储地址和快照,对于每个 Data Source.提供商在您创建令牌时从您的登录或服务器获取令牌,而不是从地址获取令牌。
| 服务 | 提供商 ID | 地址 | 凭据 |
|---|---|---|---|
| Google 表格 | google-sheets | { spreadsheetId, range } | 用户的 Google 登录 (OAuth) |
| Excel | excel | { itemId, table } 或 { itemId, worksheet, range } | 用户的 Microsoft 登录 (MSAL) |
| Airtable | airtable | { baseId, table, fields } | 服务器上的令牌 |
您选择提供商 ID。 编写提供者 表示提供者必须具备的内容。
Google 表格
已发布的工作表,没有凭据
以 CSV 形式发布的工作表不需要您自己的提供商。内置 url-csv 提供程序读取它。
- 在 Google 表格中,选择“文件”、“共享”、“发布到网络”。
- 选择工作表和逗号分隔值 (.csv),然后发布。
- 将图表链接到其显示的 URL。
editor.commands.linkChartSource({
provider: 'url-csv',
address: {
url: 'https://docs.google.com/spreadsheets/d/e/2PACX-…/pub?gid=0&single=true&output=csv',
},
readOnly: true,
})- 知道 URL 的任何人都可以阅读已发布的工作表。
- Google 在更改后几分钟更新已发布的工作表,而不是立即更新。
通过 Sheets API 的私有工作表
要设置的内容:
- 启用了 Google Sheets API 的 Google Cloud 项目。
- Web 应用程序类型的 OAuth 客户端 ID,您的网站作为授权的 JavaScript 源。
- 范围
https://www.googleapis.com/auth/spreadsheets.readonly。 - 返回访问令牌的登录,例如 Google Identity Services 的令牌客户端。
提供程序读取范围 spreadsheets.values.get:
import {
ChartSourceError,
gridToChartData,
type ChartSourceProvider,
} from '@tiptap-pro/extension-diagrams/core'
function sheetAddress(address: unknown): { spreadsheetId: string; range: string } {
if (
typeof address === 'object' &&
address !== null &&
'spreadsheetId' in address &&
typeof address.spreadsheetId === 'string' &&
'range' in address &&
typeof address.range === 'string'
) {
return { spreadsheetId: address.spreadsheetId, range: address.range }
}
throw new ChartSourceError('The address needs a spreadsheetId and a range.')
}
/** Reads a range of a Google Sheet. `getAccessToken` comes from your own Google sign-in. */
export function googleSheetsSource(options: {
getAccessToken: () => Promise<string>
}): ChartSourceProvider {
return {
id: 'google-sheets',
read: async (address, { fetch }) => {
const { spreadsheetId, range } = sheetAddress(address)
const url =
`https://sheets.googleapis.com/v4/spreadsheets/${encodeURIComponent(spreadsheetId)}` +
`/values/${encodeURIComponent(range)}` +
'?valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=FORMATTED_STRING'
const response = await fetch(url, {
headers: { Authorization: `Bearer ${await options.getAccessToken()}` },
})
if (!response.ok) throw new ChartSourceError(`Google Sheets answered ${response.status}.`)
const { values = [] }: { values?: unknown[][] } = await response.json()
return gridToChartData(
values.map((row) => row.map((cell) => String(cell ?? ''))),
{ decimalSeparator: '.' },
)
},
describe: (address) => `Google Sheet, ${sheetAddress(address).range}`,
}
}注册它并链接图表:
getGoogleAccessToken是返回登录用户的访问令牌的函数。spreadsheetId是工作表 URL 中的长 ID。range采用 A1 表示法,带有工作表名称。
import { Diagrams } from '@tiptap-pro/extension-diagrams'
Diagrams.configure({
sources: [googleSheetsSource({ getAccessToken: getGoogleAccessToken })],
})
editor.commands.linkChartSource({
provider: 'google-sheets',
address: { spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms', range: 'Sales!A1:D13' },
readOnly: true,
})第一行是标题,第一列包含标签。数字以数字形式到达,因为 UNFORMATTED_VALUE 返回 1200.5 而不是 $1,200.50。日期如表所示通过 FORMATTED_STRING 到达。 API 在行末尾留下了空单元格,并且图表将它们显示为无值。
Sheets API 允许每个项目每分钟 300 个读取请求,每个用户每分钟 60 个。除此之外,它会回答 429。
Excel
提供程序通过 Microsoft Graph 读取 OneDrive 或 SharePoint 中的 Excel 工作簿。
设置内容:
- Microsoft Entra ID 中的应用程序注册,具有单页应用程序重定向 URI。
- 委派权限
Files.ReadWrite。提供程序仅读取,但 Microsoft Graph 将Files.ReadWrite列为读取范围的权限。 - MSAL (
@azure/msal-browser) 用于让用户登录并获取令牌。 .xlsx文件形式的工作簿。其itemId是 Microsoft Graph 为文件返回的驱动器项目 ID。
import {
ChartSourceError,
gridToChartData,
type ChartSourceProvider,
} from '@tiptap-pro/extension-diagrams/core'
const text = (value: unknown): string => (typeof value === 'string' ? value : '')
/** `{ itemId, table }` or `{ itemId, worksheet, range }`, with a `driveId` for SharePoint. */
function rangePath(address: unknown): string {
const field = (key: string) =>
typeof address === 'object' && address !== null && key in address
? text(Reflect.get(address, key))
: ''
if (!field('itemId')) throw new ChartSourceError('The address needs an itemId.')
const drive = field('driveId') ? `drives/${field('driveId')}` : 'me/drive'
const workbook = `${drive}/items/${field('itemId')}/workbook`
if (field('table')) return `${workbook}/tables/${encodeURIComponent(field('table'))}/range`
const sheet = encodeURIComponent(field('worksheet'))
return `${workbook}/worksheets/${sheet}/range(address='${field('range')}')`
}
/** Reads an Excel workbook in OneDrive or SharePoint. `getAccessToken` comes from MSAL. */
export function excelSource(options: {
getAccessToken: () => Promise<string>
}): ChartSourceProvider {
return {
id: 'excel',
read: async (address, { fetch }) => {
const url = `https://graph.microsoft.com/v1.0/${rangePath(address)}?$select=values,text`
const response = await fetch(url, {
headers: { Authorization: `Bearer ${await options.getAccessToken()}` },
})
if (!response.ok) throw new ChartSourceError(`Microsoft Graph answered ${response.status}.`)
const range: { values: unknown[][]; text: string[][] } = await response.json()
// A date is a number in Excel, so the label column is read as the workbook shows it
return gridToChartData(
range.values.map((row, r) =>
row.map((value, c) =>
c > 0 && typeof value === 'number' ? String(value) : text(range.text[r]?.[c]),
),
),
{ decimalSeparator: '.' },
)
},
}
}来自 MSAL 的令牌,可以时以静默方式登录,必须时会弹出窗口:
import { PublicClientApplication } from '@azure/msal-browser'
const msal = new PublicClientApplication({ auth: { clientId: 'your-client-id' } })
await msal.initialize()
async function getAccessToken(): Promise<string> {
const request = { scopes: ['Files.ReadWrite'], account: msal.getAllAccounts()[0] }
try {
return (await msal.acquireTokenSilent(request)).accessToken
} catch {
return (await msal.acquireTokenPopup(request)).accessToken
}
}注册它并将图表链接到名为 Sales 的表:
Diagrams.configure({ sources: [excelSource({ getAccessToken })] })
editor.commands.linkChartSource({
provider: 'excel',
address: { itemId: '01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36K', table: 'Sales' },
readOnly: true,
})首选表作为地址。表会随着新行的增加而增长,而像 A1:D13 这样的范围则不会。 SharePoint 中的工作簿或与用户共享的工作簿也需要在地址中包含 driveId。
数字来自 values,标签来自 text,如工作簿所示。
Microsoft Graph 允许一个租户中每个应用每 10 秒发出 1,500 个请求。除此之外,它会回答 429。
Airtable
提供商通过服务器上的路由读取 Airtable 表的记录,这会添加令牌。
设置内容:
- 具有
data.records:read范围和对基础的访问权限的个人访问令牌。它保留在您的服务器上。 - 基本 ID (
app…) 和表名称或 ID (tbl…),均位于表的 URL 中。 - 您服务器上的以下路由。
打开应用程序网页的任何人都可以在那里读取令牌,并且令牌会到达它有权访问的每个基地。该路由仅发送 GET 请求,并且仅发送到您允许的基础:
/**
* Runs on your server. Forwards `GET /api/airtable/<baseId>/<table>?…` to Airtable with a
* personal access token, for the bases you allow.
*/
export function airtableProxy(options: {
token: string
bases: string[]
}): (request: Request) => Promise<Response> {
return async (request) => {
const url = new URL(request.url)
const path = url.pathname.replace(/^\/api\/airtable\//, '')
if (request.method !== 'GET' || !options.bases.includes(path.split('/')[0] ?? '')) {
return new Response('Not allowed', { status: 403 })
}
const response = await fetch(`https://api.airtable.com/v0/${path}${url.search}`, {
headers: { Authorization: `Bearer ${options.token}` },
})
return new Response(response.body, {
status: response.status,
headers: { 'Content-Type': 'application/json' },
})
}
}将其安装在您的服务器接受请求的位置,例如 app.get('/api/airtable/*', …)。检查登录用户是否可以阅读该库。
提供程序读取表的记录,每页 100 条,并跟随 offset 进入下一页:
import {
ChartSourceError,
gridToChartData,
type ChartSourceProvider,
} from '@tiptap-pro/extension-diagrams/core'
function tableAddress(address: unknown): { baseId: string; table: string; fields: string[] } {
if (
typeof address === 'object' &&
address !== null &&
'baseId' in address &&
typeof address.baseId === 'string' &&
'table' in address &&
typeof address.table === 'string' &&
'fields' in address &&
Array.isArray(address.fields)
) {
return { baseId: address.baseId, table: address.table, fields: address.fields.map(String) }
}
throw new ChartSourceError('The address needs a baseId, a table and its fields.')
}
/** Reads the records of an Airtable table through `proxyUrl`, a route of your server. */
export function airtableSource(options: { proxyUrl: string }): ChartSourceProvider {
return {
id: 'airtable',
read: async (address, { fetch }) => {
const { baseId, table, fields } = tableAddress(address)
const records: Record<string, unknown>[] = []
let offset = ''
do {
const query = new URLSearchParams(offset ? { offset } : {})
for (const field of fields) query.append('fields[]', field)
const path = `${encodeURIComponent(baseId)}/${encodeURIComponent(table)}`
const response = await fetch(`${options.proxyUrl}/${path}?${query}`)
if (!response.ok) throw new ChartSourceError(`Airtable answered ${response.status}.`)
const page: { records: { fields: Record<string, unknown> }[]; offset?: string } =
await response.json()
records.push(...page.records.map((record) => record.fields))
offset = page.offset ?? ''
} while (offset)
// Airtable leaves out empty fields, so the columns are the fields the address names
const cell = (value: unknown) =>
typeof value === 'string' || typeof value === 'number' ? String(value) : ''
return gridToChartData(
[fields, ...records.map((record) => fields.map((field) => cell(record[field])))],
{ decimalSeparator: '.' },
)
},
describe: (address) => `Airtable, ${tableAddress(address).table}`,
}
}注册它并将图表链接到表的两个字段:
Diagrams.configure({ sources: [airtableSource({ proxyUrl: '/api/airtable' })] })
editor.commands.linkChartSource({
provider: 'airtable',
address: { baseId: 'appXXXXXXXXXXXXXX', table: 'Deals', fields: ['Stage', 'Count'] },
readOnly: true,
})地址中的 fields 是按顺序排列的列。 Airtable 在记录中留下了空字段,因此记录本身并不能说明有哪些列。第一个字段包含标签。提供者读取文本和数字。任何其他字段(例如附件或链接记录)均显示为无值。
Airtable 允许每个基地每秒 5 个请求。之后,它会回答 429 并在 30 秒后再次接受请求。包含 1,000 条记录的表需要 10 个请求。
使图表保持最新
链接的图表会显示其快照,直到您将其同步。
// The selected chart
editor.commands.syncChartData()
// Every chart that reads this range
await editor.storage.diagrams.requestSync({
provider: 'google-sheets',
address: { spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms', range: 'Sales!A1:D13' },
})读取失败将转到 onSourceError 选项。上述提供商会因地址错误或服务错误答案而抛出 ChartSourceError。其他故障(例如连接丢失或令牌请求失败)会以其自己的错误类型到达。 图表同步时 具有读取失败的规则、requestSync 和setInterval 的计划以及服务限制。