Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.
Open
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
12 changes: 11 additions & 1 deletion Clients/Xamarin.Interactive.Client.Web/ClientApp/Workbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export async function loadWorkbookFromString(workbookSession: WorkbookSession, f
return new Workbook(workbookSession, fileName, content, data)
}

export async function loadWorkbookFromUrl(workbookSession: WorkbookSession, url: string): Promise<Workbook> {
var response = await fetch(url)

if (!response.ok)
throw new Error("Couldn't load the workbook")

var content = await response.text()
return await loadWorkbookFromString (workbookSession, url, content)
}

export async function loadWorkbookFromGist(workbookSession: WorkbookSession, gistUrl: string) : Promise<Workbook> {
// TODO: Specify revision to load, specify root workbook file.
const gistId = gistUrl.split('/').slice(-1)
Expand All @@ -55,7 +65,7 @@ export async function loadWorkbookFromGist(workbookSession: WorkbookSession, gis
return await loadWorkbookFromWorkbookPackage(workbookSession, workbookPackage);
}

export async function loadWorkbookFromWorkbookPackage(workbookSession: WorkbookSession, workbookPackage: File): Promise<Workbook> {
export async function loadWorkbookFromWorkbookPackage(workbookSession: WorkbookSession, workbookPackage: File): Promise<Workbook> {
const loadedZip = await loadAsync(workbookPackage)
const workbookFiles = loadedZip.filter((path, file) => path.endsWith(".workbook"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import { WorkbookShell } from './WorkbookShell';

export class Home extends React.Component<RouteComponentProps<{}>, {}> {
public render() {
return <WorkbookShell />
return <WorkbookShell/>
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Fabric } from 'office-ui-fabric-react/lib/Fabric';
import { initializeIcons } from '@uifabric/icons';
import 'office-ui-fabric-react/dist/css/fabric.css';

initializeIcons();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@ import { StatusMessageBar } from './StatusMessageBar'
import { StatusUIAction, MessageKind, MessageSeverity } from '../messages'

import './WorkbookShell.scss'
import { loadWorkbookFromString, loadWorkbookFromWorkbookPackage, loadWorkbookFromGist, Workbook } from '../Workbook';
import { loadWorkbookFromString, loadWorkbookFromWorkbookPackage, loadWorkbookFromGist, loadWorkbookFromUrl, Workbook } from '../Workbook';

export interface WorkbookShellContext {
session: WorkbookSession
rendererRegistry: ResultRendererRegistry
}

export interface WorkbookShellProps {
contentString?: string
contentUrl?: string
}

interface WorkbookShellState {
isPackageDialogHidden: boolean
}

export class WorkbookShell extends React.Component<any, WorkbookShellState> {
export class WorkbookShell extends React.Component<WorkbookShellProps, WorkbookShellState> {
private shellContext: WorkbookShellContext
private commandBar: WorkbookCommandBar | null = null
private workbookEditor: WorkbookEditor | null = null
Expand All @@ -53,6 +58,7 @@ export class WorkbookShell extends React.Component<any, WorkbookShellState> {
this.saveWorkbook = this.saveWorkbook.bind(this)
this.dumpDraftState = this.dumpDraftState.bind(this)
this.triggerGistPicker = this.triggerGistPicker.bind(this)
this.loadInitialContent = this.loadInitialContent.bind(this)

this.shellContext = {
session: new WorkbookSession,
Expand All @@ -67,13 +73,32 @@ export class WorkbookShell extends React.Component<any, WorkbookShellState> {
private onSessionEvent(session: WorkbookSession, sessionEvent: SessionEvent) {
if (sessionEvent.kind === SessionEventKind.Ready) {
this.workspaceAvailable = true
if (this.workbookEditor)
this.workbookEditor.setUpInitialState()
if (this.workbookEditor && !this.props.contentString && !this.props.contentUrl) {
this.workbookEditor.setUpInitialState()
}
// Fire and forget content load
this.loadInitialContent();
} else {
this.workspaceAvailable = false
}
}

async loadInitialContent() {
if (this.props.contentUrl && this.workbookEditor) {
if (this.props.contentUrl.startsWith('/api/gist'))
this.workbook = await loadWorkbookFromGist(this.shellContext.session, this.props.contentUrl)
else
this.workbook = await loadWorkbookFromUrl(this.shellContext.session, this.props.contentUrl)

await this.workbookEditor.loadNewContent(this.workbook.markdownContent)
this.restoreNuGetPackages()
} else if (this.props.contentString && this.workbookEditor) {
this.workbook = await loadWorkbookFromString (this.shellContext.session, "Aco", this.props.contentString)
await this.workbookEditor.loadNewContent(this.workbook.markdownContent)
this.restoreNuGetPackages()
}
}

async componentDidMount() {
this.shellContext.session.sessionEvent.addListener(this.onSessionEvent)

Expand Down
102 changes: 98 additions & 4 deletions Clients/Xamarin.Interactive.Client.Web/ClientApp/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,104 @@
import * as React from 'react';
import { Route } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Route, Link, RouteComponentProps, RouteProps, match } from 'react-router-dom';
import { Layout, LayoutProps } from './components/Layout';
import { ImageFit } from 'office-ui-fabric-react/lib/Image'
import { Home } from './components/Home';
import { ComponentPlayground } from './components/ComponentPlayground';
import { WorkbookShell } from './components/WorkbookShell';
import { DocumentCard, DocumentCardTitle, DocumentCardLogo, DocumentCardPreview } from 'office-ui-fabric-react/lib/DocumentCard';
import { createLocation } from "history";

export interface CardProps extends RouteProps {
cardId: string,
}

export interface CardCatalog {
cardId: string
title: string
icon?: string
contentString?: string
contentUrl?: string
}

export class CardItem extends React.Component<RouteComponentProps<CardProps>, {}> {
getProps(cardId: string): any {
var match = Catalog.Cards.find((card) => {
return card.cardId === cardId
})

if (match === undefined)
return {}

return {
contentUrl: match.contentUrl,
contentString: match.contentString
}
}

public render() {
return <WorkbookShell {...this.getProps(this.props.match.params.cardId)} />
}
}
export class Catalog extends React.Component<RouteComponentProps<CardProps>, { cards: CardCatalog[] }> {
static loaded = false
constructor() {
super();
this.state = { cards: Catalog.Cards }
}

public componentDidMount() {
var t = fetch('/api/workbook')
.then(response => response.json())
.then((newCards) => {
if (newCards) {
Catalog.Cards = newCards.map((card: CardCatalog, key: any) => {
//var old = Catalog.getCatalog(card.cardId)
return { ...card }
})
}
this.setState ({cards: newCards})
})
}

public static Cards: CardCatalog [] = []

public render() {
return <div>
<h2>Workbooks</h2>
<div className="ms-Grid">
<div className="ms-Grid-row">
{this.state.cards.map((card, key) => {
const location = createLocation(`/live/${card.cardId}`, null, undefined, this.props.history.location)
const url = this.props.history.createHref (location)
const previewProps = {
previewImages: [
{
width: 200,
height: 200,
previewImageSrc: card.icon || 'Icon.png',
imageFit: ImageFit.contain
}
]
};

return (
<div className="ms-Grid-col ms-sm6 ms-md3 ms-lg3" key={key}>
<DocumentCard onClick={() => { this.props.history.push(url)}}>
<DocumentCardPreview {...previewProps} />
<DocumentCardTitle title={card.title} />
</DocumentCard>
</div>
)
})}
</div>
</div>
<Route exact path={this.props.match.url} render={() => <h3>Select a workbook to begin</h3>} />
</div>
}
}

export const routes = <Layout>
<Route exact path='/' component={ Home } />
<Route exact path='/' component={ Catalog } />
<Route path='/live/:cardId' component={ CardItem } />
<Route path='/component-playground' component={ ComponentPlayground } />
</Layout>;
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// Author:
// Larry Ewing <lewing@microsoft.com>
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;
using System;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Configuration;

using Newtonsoft.Json.Linq;
using Newtonsoft.Json;

using Xamarin.Interactive.Core;
using Xamarin.Interactive.Workbook.Models;

namespace Xamarin.Interactive.Client.Web.Controllers
{
public class Card {
public string CardId { get; set; }
public string Icon { get; set; }
public string ContentUrl { get; set; }
public string ContentString { get; set; }
public string Guid { get; set; }
public string Title { get; set; }
public string Name { get; set; }
//public ImmutableArray<AgentType> PlatformTargets { get; set; }
//public ImmutableArray<InteractivePackageDescription> Packages { get; set; }
}

[Route("/api/workbook")]
public sealed class WorkbookController : Controller
{
readonly IConfiguration configuration;
readonly IHostingEnvironment hosting;

public WorkbookController (IConfiguration configuration, IHostingEnvironment hosting)
{
this.configuration = configuration;
this.hosting = hosting;
}

public IActionResult Index()
{
var workbooks = configuration.GetSection("workbooks");
var workbookDir = workbooks.GetValue<string> ("path");

if (String.IsNullOrEmpty (workbookDir))
return new JsonResult(new { });

var fileProvider = hosting.WebRootFileProvider;
var path = fileProvider.GetFileInfo (workbookDir);
var dir = fileProvider.GetDirectoryContents (workbookDir);

var cards = new Dictionary<string, Card>();
foreach (var book in workbooks.GetSection ("books").GetChildren ()) {
var card = new Card
{
CardId = book.GetValue<string>(nameof(Card.CardId)),
ContentUrl = book.GetValue<string>(nameof(Card.ContentUrl)),
ContentString = book.GetValue<string>(nameof(Card.ContentString)),
Title = book.GetValue<string> (nameof (Card.Title)),
Name = book.GetValue<string> (nameof (Card.Name)),
Icon = book.GetValue<string> (nameof (Card.Icon))
};
if (!String.IsNullOrEmpty (card.Name))
cards[card.Name] = card;

card.CardId = card.CardId ?? card.Name.Replace(".workbook", "");
}

var contents = dir.Where (book => !book.IsDirectory && book.PhysicalPath.EndsWith (".workbook")).Select(book =>
{
var reader = new StreamReader(book.CreateReadStream ());
var document = new WorkbookDocument();
document.Read(reader);

var manifest = new WorkbookDocumentManifest();
manifest.Read(document);

return new Card
{
CardId = book.Name.Replace(".workbook", ""),
ContentUrl = book.PhysicalPath.Replace (hosting.WebRootPath, ""),
Name = book.Name,
Title = manifest.Title,
Guid = manifest.Guid.ToString (),
};
});

foreach (var card in contents) {
if (cards.TryGetValue (card.Name, out var configCard)) {
configCard.Guid = card.Guid;
configCard.Title = card.Title ?? configCard.Title;
configCard.ContentUrl = card.ContentUrl;
configCard.CardId = configCard.CardId ?? configCard.Name.Replace(".workbook", "");
} else {
cards[card.Name] = card;
}
}

return new JsonResult(cards.Values);
}
}
}
Loading