Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runme is getting panels #677

Merged
merged 5 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions __mocks__/vscode-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export const TelemetryReporter = {
sendTelemetryEvent: vi.fn(),
sendTelemetryErrorEvent: vi.fn()
}

export class TelemetryViewProvider {
constructor() {
}
}
1 change: 1 addition & 0 deletions __mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const window = {
onDidChangeActiveNotebookEditor: vi.fn().mockReturnValue({ dispose: vi.fn() }),
registerTreeDataProvider: vi.fn(),
registerUriHandler: vi.fn(),
registerWebviewViewProvider: vi.fn(),
onDidCloseTerminal: vi.fn(),
withProgress: vi.fn(),
onDidChangeActiveColorTheme: vi.fn().mockReturnValue({ dispose: vi.fn() }),
Expand Down
6 changes: 6 additions & 0 deletions assets/runme-logo-sidebar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -560,13 +560,31 @@
}
}
],
"viewsContainers": {
"activitybar": [
{
"id": "runme",
"title": "Runme",
"icon": "assets/runme-logo-sidebar.svg"
}
]
},
"views": {
"explorer": [
{
"id": "runme.launcher",
"type": "tree",
"name": "Runme Notebooks",
"visibility": "collapsed"
}
],
"runme": [
{
"id": "runme.cloud",
"type": "webview",
"name": "Cloud",
"visibility": "visible"
}
]
},
"viewsWelcome": [
Expand Down Expand Up @@ -683,6 +701,7 @@
"lit": "^2.7.4",
"octokit": "^2.0.14",
"preact": "^10.14.1",
"tangle": "^3.0.0",
"undici": "^5.22.1",
"uuid": "^9.0.0",
"vercel": "^30.1.2",
Expand Down
14 changes: 13 additions & 1 deletion src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Disposable, workspace, notebooks, commands, ExtensionContext, tasks, window } from 'vscode'
import { TelemetryReporter } from 'vscode-telemetry'
import Channel from 'tangle/webviews'

import { Serializer } from '../types'
import { Serializer, SyncSchema } from '../types'
import { registerExtensionEnvironmentVariables } from '../utils/configuration'

import { Kernel } from './kernel'
Expand Down Expand Up @@ -40,6 +41,7 @@ import { GrpcRunner, IRunner } from './runner'
import { CliProvider } from './provider/cli'
import * as survey from './survey'
import { RunmeCodeLensProvider } from './provider/codelens'
import Panel from './panels/panel'

export class RunmeExtension {
async initialize(context: ExtensionContext) {
Expand Down Expand Up @@ -118,6 +120,7 @@ export class RunmeExtension {
kernel,
serializer,
server,
...this.registerPanels(context),
treeViewer,
...surveys,
workspace.registerNotebookSerializer(Kernel.type, serializer, {
Expand Down Expand Up @@ -200,6 +203,15 @@ export class RunmeExtension {
await bootFile()
}

protected registerPanels(context: ExtensionContext): Disposable[] {
const id: string = 'runme.cloud' as const
const channel = new Channel<SyncSchema>('app')
const p = new Panel(context, id)
const bus$ = channel.register([p.webview])
p.registerBus(bus$)
return [window.registerWebviewViewProvider(id, p), p]
}

static registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any) {
return commands.registerCommand(
command,
Expand Down
15 changes: 4 additions & 11 deletions src/extension/messages/cloudApiRequest/saveCellExecution.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { authentication } from 'vscode'
import { TelemetryReporter } from 'vscode-telemetry'

import { AuthenticationProviders, ClientMessages } from '../../../constants'
import { ClientMessages } from '../../../constants'
import { ClientMessage, IApiMessage } from '../../../types'
import { InitializeClient } from '../../api/client'
import { getCellByUuId } from '../../cell'
import { getAnnotations, getCellRunmeId } from '../../utils'
import { getAnnotations, getAuthSession, getCellRunmeId } from '../../utils'
import { postClientMessage } from '../../../utils/messaging'
import { RunmeService } from '../../services/runme'
import { CreateCellExecutionDocument } from '../../__generated__/graphql'
Expand All @@ -20,13 +19,7 @@ export default async function saveCellExecution(
const { messaging, message, editor } = requestMessage

try {
const session = await authentication.getSession(
AuthenticationProviders.GitHub,
['user:email'],
{
createIfNone: true,
}
)
const session = await getAuthSession()

if (!session) {
throw new Error('You must authenticate with your GitHub account')
Expand All @@ -51,7 +44,7 @@ export default async function saveCellExecution(
const annotations = getAnnotations(cell)
delete annotations['runme.dev/uuid']
const runmeService = new RunmeService({ githubAccessToken: session.accessToken })
const runmeTokenResponse = await runmeService.getAccessToken()
const runmeTokenResponse = await runmeService.getUserToken()
if (!runmeTokenResponse) {
throw new Error('Unable to retrieve an access token')
}
Expand Down
126 changes: 126 additions & 0 deletions src/extension/panels/panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Disposable, ExtensionContext, Webview, WebviewView, WebviewViewProvider } from 'vscode'
import { TelemetryViewProvider } from 'vscode-telemetry'
import { Subject } from 'rxjs'
import { Observable, Subscription } from 'rxjs'

import { fetchStaticHtml, getAuthSession } from '../utils'
import { IAppToken, RunmeService } from '../services/runme'
import { SyncSchemaBus } from '../../types'

export type DefaultUx = 'panels'
export interface InitPayload {
ide: 'code'
panelId: string
appToken: string | null
defaultUx: DefaultUx
}

class PanelBase extends TelemetryViewProvider implements Disposable {
protected readonly appUrl: string = 'http://localhost:3001'
protected readonly defaultUx: DefaultUx = 'panels'

constructor(protected readonly context: ExtensionContext) {
super()
}

public dispose() {}

public async getAppToken(createIfNone: boolean = true): Promise<IAppToken | null> {
const session = await getAuthSession(createIfNone)

if (session) {
const service = new RunmeService({ githubAccessToken: session.accessToken })
const userToken = await service.getUserToken()
return await service.getAppToken(userToken)
}

return null
}

public hydrateHtml(html: string, payload: InitPayload) {
let content = html
// eslint-disable-next-line quotes
content = html.replace(`'{ "appToken": null }'`, `'${JSON.stringify(payload)}'`)
content = content.replace(
'<script id="appAuthToken">',
`<base href="${this.appUrl}"><script id="appAuthToken">`
)
return content
}
}

export default class Panel extends PanelBase implements WebviewViewProvider {
public readonly webview = new Subject<Webview>()
protected readonly staticHtml

constructor(protected readonly context: ExtensionContext, public readonly identifier: string) {
super(context)

this.staticHtml = fetchStaticHtml(this.appUrl)
}

async resolveWebviewTelemetryView(webviewView: WebviewView): Promise<void> {
const webviewOptions = {
enableScripts: true,
retainContextWhenHidden: true,
}

console.log(`${this.identifier} webview resolving`)

const html = await this.getHydratedHtml()
webviewView.webview.html = html
webviewView.webview.options = {
...webviewOptions,
localResourceRoots: [this.context.extensionUri],
}

this.webview.next(webviewView.webview)
console.log(`${this.identifier} webview resolved`)
return Promise.resolve()
}

private async getHydratedHtml(): Promise<string> {
let appToken: string | null
let staticHtml: string
try {
appToken = await this.getAppToken(false).then((appToken) => appToken?.token ?? null)
staticHtml = await this.staticHtml.then((r) => r.text())
} catch (err: any) {
console.error(err?.message || err)
throw err
}
return this.hydrateHtml(staticHtml, {
ide: 'code',
panelId: this.identifier,
appToken: appToken ?? 'EMPTY',
defaultUx: this.defaultUx,
})
}

// unnest existing type would be cleaner
private async onSignIn(bus: SyncSchemaBus) {
try {
const appToken = await this.getAppToken(true)
bus.emit('onAppToken', appToken!)
} catch (err: any) {
console.error(err?.message || err)
}
}

public registerBus(bus$: Observable<SyncSchemaBus>) {
bus$.subscribe((bus) => {
const subs: Subscription[] = [
bus.on('onCommand', (cmdEvent) => {
if (cmdEvent?.name !== 'signIn') {
return
}
this.onSignIn(bus)
}),
]

return () => {
subs.forEach((s) => s.unsubscribe())
}
})
}
}
40 changes: 34 additions & 6 deletions src/extension/services/runme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import fetch from 'cross-fetch'

import { getRunmeApiUrl } from '../../utils/configuration'

export interface IRunmeToken {
export interface IUserToken {
token: string
}

export interface IAppToken {
token: string
}

Expand All @@ -11,9 +15,10 @@ export class RunmeService {
constructor({ githubAccessToken }: { githubAccessToken: string }) {
this.githubAccessToken = githubAccessToken
}
async getAccessToken(): Promise<IRunmeToken | undefined> {
async getUserToken(): Promise<IUserToken> {
let response
try {
const response = await fetch(`${getRunmeApiUrl()}/auth/vscode`, {
response = await fetch(`${getRunmeApiUrl()}/auth/vscode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -23,13 +28,36 @@ export class RunmeService {
}),
})
if (response.status >= 400) {
throw new Error('Failed to get an authorization token')
throw new Error('Failed to get an user authorization token')
}
if (response.ok) {
return response.json()
if (!response.ok) {
throw new Error('Request to user authorization endpoint failed')
}
} catch (error) {
throw new Error((error as any).message)
}

return response.json()
}
async getAppToken(userToken: IUserToken): Promise<IAppToken> {
let response
try {
response = await fetch(`${getRunmeApiUrl()}/auth/user/app`, {
method: 'POST',
headers: {
Authorization: `Bearer ${userToken.token}`,
},
})
if (response.status >= 400) {
throw new Error('Failed to get an app authorization token')
}
if (!response.ok) {
throw new Error('Request to app authorization endpoint failed')
}
} catch (error) {
throw new Error((error as any).message)
}

return response.json()
}
}
18 changes: 17 additions & 1 deletion src/extension/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import util from 'node:util'
import cp from 'node:child_process'
import os from 'node:os'

import { fetch } from 'cross-fetch'
import vscode, {
FileType,
Uri,
Expand All @@ -14,6 +15,7 @@ import vscode, {
commands,
WorkspaceFolder,
ExtensionContext,
authentication,
} from 'vscode'
import { v5 as uuidv5 } from 'uuid'
import getPort from 'get-port'
Expand All @@ -27,7 +29,11 @@ import {
ShellType,
} from '../types'
import { SafeCellAnnotationsSchema, CellAnnotationsSchema } from '../schema'
import { NOTEBOOK_AVAILABLE_CATEGORIES, SERVER_ADDRESS } from '../constants'
import {
AuthenticationProviders,
NOTEBOOK_AVAILABLE_CATEGORIES,
SERVER_ADDRESS,
} from '../constants'
import {
getEnvLoadWorkspaceFiles,
getEnvWorkspaceFileOrder,
Expand Down Expand Up @@ -554,3 +560,13 @@ export function convertEnvList(envs: string[]): Record<string, string | undefine
return prev
}, {} as Record<string, string | undefined>)
}

export function getAuthSession(createIfNone: boolean = true) {
return authentication.getSession(AuthenticationProviders.GitHub, ['user:email'], {
createIfNone,
})
}

export function fetchStaticHtml(appUrl: string) {
return fetch(appUrl)
}
Loading