Skip to content

Commit

Permalink
[KOGITO-1567] Export source code as GitHub gist.
Browse files Browse the repository at this point in the history
  • Loading branch information
caponetto committed Apr 1, 2020
1 parent ed0586c commit 6a89711
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 24 deletions.
5 changes: 4 additions & 1 deletion packages/online-editor/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useCallback, useMemo, useState } from "react";
import { Route, Switch } from "react-router";
import { HashRouter } from "react-router-dom";
import { Routes } from "./common/Routes";
import { GithubService } from "./common/GithubService";
import { HomePage } from "./home/HomePage";
import { EditorPage } from "./editor/EditorPage";
import { NoMatchPage } from "./NoMatchPage";
Expand All @@ -38,6 +39,7 @@ interface Props {
readonly: boolean;
external: boolean;
senderTabId?: string;
githubService: GithubService;
}

export function App(props: Props) {
Expand Down Expand Up @@ -79,7 +81,8 @@ export function App(props: Props) {
file: file,
readonly: props.readonly,
external: props.external,
senderTabId: props.senderTabId
senderTabId: props.senderTabId,
githubService: props.githubService
}}
>
<HashRouter>
Expand Down
28 changes: 28 additions & 0 deletions packages/online-editor/src/__tests__/GithubService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,31 @@ describe("githubService::retrieveFileInfo", () => {
expect(fileInfo.path).toEqual("the_file.bpmn");
});
});

describe("githubService::isGist", () => {
test("should be true", () => {
[
"https://gist.github.com/user/gist_id",
"http://gist.github.com/user/gist_id",
"http://www.gist.github.com/user/gist_id",
"https://www.gist.github.com/user/gist_id",
"www.gist.github.com/user/gist_id",
"gist.github.com/user/gist_id"
].forEach(url => expect(githubService.isGist(url)).toBeTruthy());
});

test("should be false", () => {
[
"https://gist.gathub.com/user/gist_id",
"http://gist.redhat.com/user/gist_id"
].forEach(url => expect(githubService.isGist(url)).toBeFalsy());
});
});

describe("githubService::extractGistId", () => {
test("check gist id", () => {
const fileUrl = "https://gist.github.com/user/gist_id";
const gistId = githubService.extractGistId(fileUrl);
expect(gistId).toEqual("gist_id");
});
});
106 changes: 104 additions & 2 deletions packages/online-editor/src/common/GithubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,95 @@
*/

import * as Octokit from "@octokit/rest";
import { getCookie, setCookie } from "./utils";

export const GITHUB_OAUTH_TOKEN_SIZE = 40;
export const GITHUB_TOKENS_URL = "https://github.com/settings/tokens";
export const GITHUB_TOKENS_HOW_TO_URL =
"https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line";

const GITHUB_AUTH_TOKEN_COOKIE_NAME = "github-oauth-token-kie-editors";
const EMPTY_TOKEN = "";

export interface FileInfo {
gitRef: string;
repo: string;
org: string;
path: string;
}

export class GithubService {
private readonly octokit: Octokit;
private octokit: Octokit;
private authenticated: boolean;

constructor() {
private init(): void {
this.octokit = new Octokit();
this.authenticated = false;

setCookie(GITHUB_AUTH_TOKEN_COOKIE_NAME, EMPTY_TOKEN);
}

private initAuthenticated(token: string): void {
this.octokit = new Octokit({ auth: token });
this.authenticated = true;

setCookie(GITHUB_AUTH_TOKEN_COOKIE_NAME, token);
}

private validateToken(token: string): Promise<boolean> {
if (!token) {
return Promise.resolve(false);
}

const testOctokit = new Octokit({ auth: token });
return testOctokit.emojis
.get({})
.then(() => Promise.resolve(true))
.catch(() => Promise.resolve(false));
}

public reset(): void {
this.init();
}

public authenticate(token: string = EMPTY_TOKEN): Promise<boolean> {
token = this.resolveToken(token);

return this.validateToken(token).then(isValid => {
if (isValid) {
this.initAuthenticated(token);
} else {
this.init();
}

return Promise.resolve(isValid);
});
}

public resolveToken(token: string = EMPTY_TOKEN): string {
if (!token) {
token = getCookie(GITHUB_AUTH_TOKEN_COOKIE_NAME) || EMPTY_TOKEN;
}

return token;
}

public isAuthenticated(): boolean {
return this.authenticated;
}

public isGithub(url: string): boolean {
return /^(http:\/\/|https:\/\/)?(www\.)?github.com.*$/.test(url);
}

public isGist(url: string): boolean {
return /^(http:\/\/|https:\/\/)?(www\.)?gist.github.com.*$/.test(url);
}

public extractGistId(url: string): string {
return url.substr(url.lastIndexOf("/") + 1);
}

public retrieveFileInfo(fileUrl: string): FileInfo {
const split = new URL(fileUrl).pathname.split("/");
return {
Expand Down Expand Up @@ -64,4 +135,35 @@ export class GithubService {
).then(res => (res.ok ? res.text() : Promise.reject("Not able to retrieve file content from Github.")));
});
}

public createGist(filename: string, content: string, description: string, publicGist: boolean): Promise<string> {
if (!this.isAuthenticated()) {
return Promise.reject("User not authenticated.");
}

const gistContent: any = {
description: description,
public: publicGist,
files: {
[filename]: {
content: content
}
}
};

return this.octokit.gists
.create(gistContent)
.then(response => Promise.resolve(response.data.files[filename].raw_url))
.catch(e => Promise.reject("Not able to create gist on Github."));
}

public getGistRawUrlFromId(gistId: string): Promise<string> {
return this.octokit.gists
.get({ gist_id: gistId })
.then(response => {
const filename = Object.keys(response.data.files)[0];
return Promise.resolve(response.data.files[filename].raw_url);
})
.catch(e => Promise.reject("Not able to get gist from Github."));
}
}
180 changes: 180 additions & 0 deletions packages/online-editor/src/common/GithubTokenModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from "react";
import { useCallback, useContext, useState } from "react";
import { GlobalContext } from "./GlobalContext";

import {
Modal,
Button,
BaseSizes,
Title,
TitleLevel,
InputGroup,
TextInput,
InputGroupText
} from "@patternfly/react-core";
import { ExternalLinkAltIcon, CheckIcon } from "@patternfly/react-icons";

import {
GITHUB_OAUTH_TOKEN_SIZE,
GITHUB_TOKENS_URL,
GITHUB_TOKENS_HOW_TO_URL
} from './GithubService';

interface Props {
onClose: () => void;
onContinue: () => void;
}

export function GithubTokenModal(props: Props) {
const context = useContext(GlobalContext);

const [potentialToken, setPotentialToken] = useState("");
const [open, setOpen] = useState(true);
const [authenticated, setAuthenticated] = useState(context.githubService.isAuthenticated());

const tokenToDisplay = obfuscate(context.githubService.resolveToken() || potentialToken);

const onPasteHandler = useCallback(e => {
const token = e.clipboardData.getData("text/plain").slice(0, GITHUB_OAUTH_TOKEN_SIZE);
setPotentialToken(token);
setTimeout(async () => {
context.githubService.authenticate(token)
.then(isAuthenticated => setAuthenticated(isAuthenticated))
}, 0);
}, []);

const onContinueHandler = useCallback(() => {
close();
props.onContinue();
}, []);

const onCloseHandler = useCallback(() => {
close();
}, []);

const onResetHandler = useCallback(() => {
context.githubService.reset();
setPotentialToken("");
setAuthenticated(false);
}, []);

const close = useCallback(() => {
props.onClose();
setOpen(false);
}, []);

const header = (
<React.Fragment>
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
GitHub OAuth Token
</Title>
<p className="pf-u-pt-sm">Authentication required for exporting to GitHub gist.</p>
</React.Fragment>
);

const content = (
<React.Fragment>
<p>
<span className="pf-u-mr-sm">
By authenticating with your OAuth Token we are able to create gists
so you can share your diagrams with your colleagues.
The token you provide is locally stored as browser cookies and it is never shared with anyone.
</span>
<a href={GITHUB_TOKENS_HOW_TO_URL} target={"_blank"}>
Learn more about GitHub tokens<ExternalLinkAltIcon className="pf-u-mx-sm" />
</a>
</p>
<br />
<p>
<b><u>NOTE:</u>&nbsp;</b>
You should provide a token with the <b>'gist'</b> permission.
</p>
</React.Fragment>
)

const footer = (
<div className="pf-u-w-100">
<h3>
<a href={GITHUB_TOKENS_URL} target={"_blank"}>
Create a new token<ExternalLinkAltIcon className="pf-u-mx-sm" />
</a>
</h3>
<InputGroup className="pf-u-mt-sm">
<TextInput
id="token-input"
name="tokenInput"
aria-describedby="token-text-input-helper"
placeholder="Paste your token here"
maxLength={GITHUB_OAUTH_TOKEN_SIZE}
isDisabled={authenticated}
isValid={!!authenticated}
value={tokenToDisplay}
onPaste={onPasteHandler}
onChange={() => { /**/ }}
autoFocus={true}
/>
{authenticated && (
<InputGroupText style={{ border: "none", backgroundColor: "#ededed" }}>
<CheckIcon />
</InputGroupText>
)}

</InputGroup>
<div className="pf-u-mt-md pf-u-mb-0 pf-u-float-right">
<Button
variant="danger"
onClick={onResetHandler}>
Reset
</Button>
<Button
className="pf-u-ml-sm"
variant="primary"
isDisabled={!authenticated}
onClick={onContinueHandler}>
Continue
</Button>
</div>
</div >
);

return (
<React.Fragment>
<Modal
isSmall={true}
isOpen={open}
onClose={onCloseHandler}
header={header}
title=""
footer={footer}
>
{content}
</Modal>
</React.Fragment>
);
}

function obfuscate(token: string) {
if (token.length <= 8) {
return token;
}

const stars = new Array(token.length - 8).join("*");
const pieceToObfuscate = token.substring(4, token.length - 4);
return token.replace(pieceToObfuscate, stars);
}
2 changes: 2 additions & 0 deletions packages/online-editor/src/common/GlobalContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as React from "react";
import { Router } from "@kogito-tooling/core-api";
import { File } from "./File";
import { Routes } from "./Routes";
import { GithubService } from "./GithubService";
import { EnvelopeBusOuterMessageHandlerFactory } from "../editor/EnvelopeBusOuterMessageHandlerFactory";

export interface GlobalContextType {
Expand All @@ -29,6 +30,7 @@ export interface GlobalContextType {
readonly: boolean;
external: boolean;
senderTabId?: string;
githubService: GithubService;
}

export const GlobalContext = React.createContext<GlobalContextType>({} as any);

0 comments on commit 6a89711

Please sign in to comment.