├── eslintignore ├── config ├── webpack.dev.config.js └── webpack.production.config.js ├── .npmignore ├── i18n ├── index.json ├── en_US │ ├── index.js │ └── global.json └── zh_CN │ ├── index.js │ └── global.json ├── CONTRIBUTING.md ├── .yarnrc ├── .gitignore ├── .npmrc ├── src ├── base-theme │ ├── index.styl │ └── styles │ │ ├── ui-variables.styl │ │ └── accessUrl.styl ├── index.js ├── app.jsx ├── api.js ├── reducer.js ├── manager.js ├── actions.js └── AccessUrl.jsx ├── .babelrc ├── .screenshots └── pluginPreview.png ├── CHANGELOG ├── .vscode └── tasks.json ├── README.md ├── .eslintrc └── package.json /eslintignore: -------------------------------------------------------------------------------- 1 | webpack* -------------------------------------------------------------------------------- /config/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .babelrc 3 | -------------------------------------------------------------------------------- /i18n/index.json: -------------------------------------------------------------------------------- 1 | ["en_US", "zh_CN"] -------------------------------------------------------------------------------- /config/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WebIde Extension -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npm.taobao.org/" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.bak 4 | .idea 5 | packages 6 | dist 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org/ 2 | #disturl=https://npm.taobao.org/dist -------------------------------------------------------------------------------- /src/base-theme/index.styl: -------------------------------------------------------------------------------- 1 | @import './styles/ui-variables'; 2 | @import './styles/accessUrl'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", {"modules": false}], 4 | "react", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /.screenshots/pluginPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/WebIDE-Plugin-AccessUrl/master/.screenshots/pluginPreview.png -------------------------------------------------------------------------------- /i18n/en_US/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | const contents = ['global']; 3 | 4 | export default contents.reduce((p, v) => { 5 | p[v] = require(`./${v}.json`); 6 | return p 7 | }, {}); 8 | -------------------------------------------------------------------------------- /i18n/zh_CN/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | const contents = ['global']; 3 | 4 | export default contents.reduce((p, v) => { 5 | p[v] = require(`./${v}.json`); 6 | return p 7 | }, {}); 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./base-theme/index.styl'); 2 | const app = require('./app').default; 3 | const Manager = require('./manager').default; 4 | const appRegistry = require('webide-plugin-sdk/utils').appRegistry; 5 | 6 | appRegistry({ 7 | app, 8 | Manager, 9 | key: 'access-url', 10 | }); 11 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | title: 更新日志 4 | toc: false 5 | timeline: true 6 | --- 7 | # Changelog 8 | All notable changes to this project will be documented in this file. 9 | 10 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 11 | and this project adheres to [Semantic Versioning](http://semver.org/). 12 | 13 | ## 0.1.0 14 | ADDED: 15 | - 修改 inject 方式 16 | 17 | ## 0.0.1-alpha.06 18 | ADDED: 19 | - 增加i18n支持 20 | 21 | ## 0.0.1-alpha.05 22 | ADDED: 23 | - injectComponent 支持 24 | 25 | ## 0.0.1 26 | 27 | ### Main 28 | ADDED: 增加基础的sdk功能 29 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { default as AccessUrl } from './AccessUrl'; 4 | import { global } from './manager'; 5 | 6 | import reducer from './reducer'; 7 | 8 | 9 | AccessUrl.propTypes = { 10 | name: PropTypes.string, 11 | style: PropTypes.object, 12 | }; 13 | export const store = global.getStoreByReducer(reducer) 14 | 15 | const app = props => ( 16 | 17 | 18 | 19 | ); 20 | 21 | export default React.createElement(app) 22 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import { global } from './manager'; 2 | 3 | const request = global.request; 4 | const config = global.sdk.config; 5 | 6 | export function createPort({ port }) { 7 | return request.post(`/hf/${config.spaceKey}`, { port }); 8 | } 9 | 10 | export function listPorts() { 11 | return request.get(`/hf/${config.spaceKey}`); 12 | } 13 | 14 | export function deletePort({ port }) { 15 | return request.delete(`/hf/${config.spaceKey}`, { port }); 16 | } 17 | 18 | export function savePort({ port }) { 19 | return request.post(`/hf/${config.spaceKey}/save`, { port }); 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "always", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "install", 12 | "args": ["install"] 13 | }, 14 | { 15 | "taskName": "update", 16 | "args": ["update"] 17 | }, 18 | { 19 | "taskName": "i18n", 20 | "args": ["run", "i18n"] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebIDE-Access-Url 2 | 3 | Bring Access Url to your Coding WebIDE. 4 | 5 | ![demo](.screenshots/pluginPreview.png) 6 | 7 | ## description 8 | you can use this plugin to ports for your project container. 9 | 10 | 11 | --- 12 | 13 | ## State of the extension 14 | 15 | ---- 16 | 17 | ## how to use 18 | 19 | ```yarn``` 20 | ```yarn run dev``` 21 | 22 | ---- 23 | 24 | ## Change log 25 | You can checkout all our changes in our [change log]. 26 | 27 | If you feel that there's some icon missing please report it to the Github repository! 28 | 29 | ## Versioning 30 | vscode-icons follows [Semantic Versioning 2.0.0](http://semver.org/). 31 | 32 | **Enjoy!** -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import { 3 | PORT_OPERATING, 4 | PORT_LIST, 5 | } from './actions'; 6 | 7 | 8 | export default handleActions({ 9 | [PORT_OPERATING]: (state, action) => { 10 | return { 11 | ...state, 12 | operating: action.payload.operating, 13 | operatingMessage: action.payload.msg || '', 14 | }; 15 | }, 16 | [PORT_LIST]: (state, action) => { 17 | return { 18 | ...state, 19 | portList: action.payload.portList || [], 20 | }; 21 | }, 22 | }, { 23 | operating: false, 24 | operatingMessage: '', 25 | generateDisabled: false, 26 | portList: [], 27 | }); 28 | -------------------------------------------------------------------------------- /i18n/zh_CN/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": "访问链接", 3 | "port": "端口", 4 | "generate": "创建链接", 5 | "refresh": "刷新", 6 | "description": "访问链接可为应用程序创建一个供外部访问的链接,创建访问链接前,请在终端(Terminal)中将需要被访问的应用程序端口映射到 0.0.0.0 地址。", 7 | "help": "查看帮助", 8 | "handleDelete": { 9 | "header": "你确定删除此端口吗", 10 | "message": "你正在删除端口 {port}", 11 | "okText": "删除" 12 | }, 13 | "message": { 14 | "createFailed": "创建失败: {msg}", 15 | "createSuccess": "创建成功!", 16 | "deleteFailed": "删除失败: {msg}", 17 | "deleteSuccess": "删除成功!", 18 | "copySuccess": "复制成功!", 19 | "copyFailed": "复制失败!", 20 | "saveFailed": "永久化失败: {msg}", 21 | "saveSuccess": "永久化成功!", 22 | "memberInfo": "只有钻石会员才能生成永久链接" 23 | }, 24 | "neverExpires": "永不过期", 25 | "permanent": "转为永久", 26 | "expires": "过期: " 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | # We use _ to define private variables and methods in clases 5 | "no-underscore-dangle": 0, 6 | # This seems to be buggy we don't want eslint to check this 7 | "import/no-extraneous-dependencies": 0, 8 | # This is a depricated rule. So we turned off it. 9 | "react/require-extension": 0, 10 | # We can write JSX in anyfile we want. 11 | "react/jsx-filename-extension": 0, 12 | # We don't like this rule. 13 | "arrow-body-style": 0, 14 | # We don't like this rule. We write arrow functions only when we needed. 15 | "prefer-arrow-callback": 0, 16 | # We don't need to write function names always. 17 | "func-names": 0, 18 | # propTypes can be object 19 | "react/forbid-prop-types": 0, 20 | # class can use this 21 | "class-methods-use-this": 0, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /i18n/en_US/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": "Access URL", 3 | "port": "Port", 4 | "generate": "Generate URL", 5 | "refresh": "Refresh", 6 | "description": "Access links create a link for external access for your application. Before you create an access link, map the application port you want to access to address 0.0.0.0 in the terminal. ", 7 | "help": "View help", 8 | "handleDelete": { 9 | "header": "Are you sure to delete this port?", 10 | "message": "You're trying to delete port {port}", 11 | "okText": "Delete" 12 | }, 13 | "message": { 14 | "createFailed": "Create failed: {msg}", 15 | "createSuccess": "Create success!", 16 | "deleteFailed": "Delete failed: {msg}", 17 | "deleteSuccess": "Delete success!", 18 | "copySuccess": "Copy success!", 19 | "copyFailed": "Copy failed!", 20 | "saveFailed": "Permanent failed: {msg}", 21 | "saveSuccess": "Permanent success!", 22 | "memberInfo": "Only a diamond member can generate a permanent link" 23 | }, 24 | "neverExpires": "Never Expires", 25 | "permanent": "Permanent", 26 | "expires": "Expires: " 27 | } 28 | -------------------------------------------------------------------------------- /src/manager.js: -------------------------------------------------------------------------------- 1 | import APP from 'webide-plugin-sdk/utils'; 2 | import Manager from 'webide-plugin-sdk/Manager'; 3 | 4 | import component, { store } from './app'; 5 | 6 | const languagePool = require('../i18n/index.json').reduce((p, v) => { 7 | p[v] = require(`../i18n/${v}/index`).default; 8 | return p; 9 | }, {}); 10 | 11 | export const global = new APP({ 12 | subscribeDataArray: ['GitState'], 13 | pkgId: 'coding_web_ide_plugin', 14 | i18n: { customLanguagePool: languagePool }, 15 | }); 16 | 17 | const { injectComponent, i18n } = global; 18 | const { position, inject } = injectComponent; 19 | 20 | export default class { 21 | pluginWillMount() { 22 | inject(position.SIDEBAR.RIGHT, { 23 | text: i18n`global.sidebar`, 24 | icon: 'fa fa-external-link', 25 | key: 'access-url', 26 | actions: { 27 | onSidebarActive: () => { 28 | }, 29 | onSidebarDeactive: () => { 30 | }, 31 | }, 32 | }, extension => extension.app); 33 | } 34 | /** 35 | * this will call only when plugin is unmount 36 | * @param {} 37 | */ 38 | pluginWillUnmount() { 39 | } 40 | get component() { 41 | return component; 42 | } 43 | get appData() { 44 | return store.getState(); 45 | } 46 | get request() { 47 | return this.getRequest(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WebIDE-Plugin-AccessUrl", 3 | "version": "0.1.2", 4 | "description": "WebIDE-Plugin-AccessUrl for Coding WebIDE", 5 | "repository": { 6 | "url": "git@github.com:Coding/WebIDE-Plugin-Env.git" 7 | }, 8 | "license": "ISC", 9 | "author": "candy ", 10 | "main": "server.js", 11 | "scripts": { 12 | "serve": "coding-ide serve", 13 | "i18n": "coding-ide i18n", 14 | "build": "coding-ide build", 15 | "start": "coding-ide start", 16 | "prebuild": "yarn" 17 | }, 18 | "codingIdePackage": { 19 | "name": "access-url", 20 | "displayName": "Access URL", 21 | "description": "WebIDE-Plugin-AccessUrl for Coding WebIDE", 22 | "type": "extension" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.14.0", 26 | "babel-core": "^6.23.1", 27 | "babel-eslint": "^6.1.2", 28 | "babel-loader": "^6.2.5", 29 | "babel-plugin-transform-runtime": "^6.15.0", 30 | "babel-polyfill": "^6.23.0", 31 | "babel-preset-react": "^6.23.0", 32 | "babel-preset-stage-0": "^6.22.0", 33 | "chai": "^3.5.0", 34 | "css-loader": "^0.26.1", 35 | "enzyme": "^2.2.0", 36 | "eslint": "^3.6.0", 37 | "eslint-config-airbnb": "^12.0.0", 38 | "eslint-plugin-import": "^1.16.0", 39 | "eslint-plugin-jsx-a11y": "^2.2.2", 40 | "eslint-plugin-react": "^6.3.0", 41 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 42 | "git-url-parse": "^6.0.1", 43 | "immutable": "^3.8.1", 44 | "jsdom": "^9.5.0", 45 | "mocha": "^3.0.2", 46 | "react": "^15.3.2", 47 | "react-addons-test-utils": "^15.3.2", 48 | "react-dom": "^15.3.2", 49 | "sinon": "^1.17.6", 50 | "stylus": "^0.54.5", 51 | "stylus-loader": "^2.5.0", 52 | "webpack": "^2.2.1", 53 | "webpack-cleanup-plugin": "^0.4.1", 54 | "webpack-dev-middleware": "^1.10.1", 55 | "webpack-dev-server": "^2.4.1", 56 | "webpack-hot-middleware": "^2.17.0" 57 | }, 58 | "peerDependencies": { 59 | "react": "^0.14.7 || ^15.0.0" 60 | }, 61 | "dependencies": { 62 | "babel-preset-env": "^1.1.8", 63 | "babel-runtime": "^6.11.6", 64 | "body-parser": "^1.15.2", 65 | "classnames": "^2.2.5", 66 | "cors": "^2.8.1", 67 | "express": "^4.14.0", 68 | "fs-promise": "^1.0.0", 69 | "nodemon": "^1.11.0", 70 | "qrcode.react": "^0.7.1", 71 | "react-redux": "^4.4.6", 72 | "redux": "^3.6.0", 73 | "redux-actions": "^1.1.0", 74 | "redux-thunk": "^2.1.0", 75 | "rimraf": "^2.6.1", 76 | "socket.io": "^1.7.3", 77 | "style-loader": "^0.13.2", 78 | "webide-plugin-sdk": "https://github.com/Coding/WebIDE-Plugin-SDK.git#0.1.5" 79 | }, 80 | "engines": { 81 | "npm": "^3.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | import { createAction } from 'redux-actions'; 3 | import * as api from './api'; 4 | import { global } from './manager'; 5 | 6 | const i18n = global.i18n; 7 | const { notify, NOTIFY_TYPE } = global.sdk.Notify; 8 | 9 | export const PORT_OPERATING = 'PORT_OPERATING'; 10 | export const portOperating = createAction(PORT_OPERATING); 11 | export function setOperating({ operating, msg }) { 12 | return dispatch => dispatch(portOperating({ operating, msg })); 13 | } 14 | 15 | export const PORT_LIST = 'PORT_LIST'; 16 | export const updatePortList = createAction(PORT_LIST); 17 | export function listPorts() { 18 | return (dispatch) => { 19 | return api.listPorts() 20 | .then((res) => { 21 | dispatch(updatePortList({ portList: res })); 22 | return res; 23 | }); 24 | }; 25 | } 26 | 27 | export function createPort({ port }) { 28 | return (dispatch) => { 29 | dispatch(portOperating({ operating: true })); 30 | return api.createPort({ port }) 31 | .then((res) => { 32 | if (res.error) { 33 | notify({ 34 | notifyType: NOTIFY_TYPE.ERROR, 35 | message: i18n`global.message.createFailed${{ msg: res.msg }}`, 36 | }); 37 | } else { 38 | notify({ message: i18n`global.message.createSuccess` }); 39 | } 40 | dispatch(portOperating({ operating: false })); 41 | return dispatch(listPorts()); 42 | }); 43 | }; 44 | } 45 | 46 | export function deletePort({ port }) { 47 | return (dispatch) => { 48 | dispatch(portOperating({ operating: true })); 49 | api.deletePort({ port }) 50 | .then((res) => { 51 | if (res.error) { 52 | notify({ 53 | notifyType: NOTIFY_TYPE.ERROR, 54 | message: i18n`global.message.deleteFailed${{ msg: res.msg }}`, 55 | }); 56 | } else { 57 | notify({ message: i18n`global.message.deleteSuccess` }); 58 | } 59 | dispatch(listPorts()); 60 | dispatch(portOperating({ operating: false })); 61 | }); 62 | }; 63 | } 64 | 65 | export function savePort({ port }) { 66 | return (dispatch) => { 67 | dispatch(portOperating({ operating: true })); 68 | return api.savePort({ port }) 69 | .then((res) => { 70 | if (res.error) { 71 | notify({ 72 | notifyType: NOTIFY_TYPE.ERROR, 73 | message: i18n`global.message.saveFailed${{ msg: res.msg }}`, 74 | }); 75 | } else { 76 | notify({ message: i18n`global.message.saveSuccess` }); 77 | } 78 | 79 | dispatch(portOperating({ operating: false })); 80 | return dispatch(listPorts()); 81 | }) 82 | .catch((res) => { 83 | notify({ 84 | notifyType: NOTIFY_TYPE.ERROR, 85 | message: i18n`global.message.saveFailed${{ msg: res.msg }}`, 86 | }); 87 | }); 88 | }; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/base-theme/styles/ui-variables.styl: -------------------------------------------------------------------------------- 1 | gray($amount) { 2 | lighten(#000000, $amount); 3 | } 4 | 5 | // Text Colors 6 | $text-color = gray(30%); 7 | $text-color-subtle = gray(50%); 8 | $text-color-highlight = gray(20%); 9 | $text-color-selected = $text-color-highlight; 10 | 11 | $text-color-info = hsl(219, 79%, 66%); 12 | $text-color-success = hsl(140, 44%, 62%); 13 | $text-color-warning = hsl( 36, 60%, 72%); 14 | $text-color-error = hsl( 9, 100%, 64%); 15 | 16 | // Base colors 17 | $base-background-color = gray(96%); 18 | $base-border-color = darken($base-background-color, 8%); 19 | 20 | // Background colors 21 | $background-color-info= #0098ff; 22 | $background-color-success= #17ca65; 23 | $background-color-warning= #ff4800; 24 | $background-color-error= #c00; 25 | 26 | $background-color-highlight= hsla(0,0%,0%,.1); 27 | $background-color-selected= $background-color-highlight; 28 | $app-background-color= #fff; 29 | 30 | // Component colors 31 | $pane-item-background-color= $base-background-color; 32 | $pane-item-border-color= $base-border-color; 33 | 34 | $input-background-color= #fff; 35 | $input-border-color= $base-border-color; 36 | 37 | $tool-panel-background-color= #f4f4f4; 38 | $tool-panel-border-color= $base-border-color; 39 | 40 | $inset-panel-background-color= #eee; 41 | $inset-panel-border-color= $base-border-color; 42 | 43 | $panel-heading-background-color= #ddd; 44 | $panel-heading-border-color= transparent; 45 | 46 | $overlay-background-color= #f4f4f4; 47 | $overlay-border-color= $base-border-color; 48 | 49 | $button-background-color= #ccc; 50 | $button-background-color-hover= lighten($button-background-color, 5%); 51 | $button-background-color-selected= $button-background-color-hover; 52 | $button-border-color= #aaa; 53 | 54 | $tab-bar-background-color = $base-background-color; 55 | $tab-bar-border-color = $base-border-color; 56 | $tab-background-color = $tab-bar-background-color; 57 | $tab-background-color-active = lighten($tab-bar-background-color, 100%); 58 | $tab-border-color = $base-border-color; 59 | 60 | $tree-view-background-color= $tool-panel-background-color; 61 | $tree-view-background-color-highlight= darken($tree-view-background-color, 5%); 62 | $tree-view-border-color= $tool-panel-border-color; 63 | 64 | 65 | $menu-background-color= $app-background-color 66 | $menu-border-color= darken($base-border-color, 15%); 67 | $menu-background-color-highlight = hsl(213, 83, 58); 68 | 69 | // Site colors 70 | $ui-site-color-1= $background-color-success; // green 71 | $ui-site-color-2= $background-color-info; // blue 72 | $ui-site-color-3= $background-color-warning; // orange 73 | $ui-site-color-4= #db2ff4; // purple 74 | $ui-site-color-5= #f5e11d; // yellow 75 | 76 | // Sizes 77 | $font-size= 13px; 78 | $input-font-size= 14px; 79 | 80 | $disclosure-arrow-size= 12px; 81 | 82 | $component-padding= 10px; 83 | $component-icon-padding= 5px; 84 | $component-icon-size= 16px; 85 | $component-line-height= 25px; 86 | $component-border-radius= 2px; 87 | 88 | $tab-height= 28px; 89 | 90 | // Other 91 | $font-family= 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif; 92 | $use-custom-controls= true; // false uses native controls 93 | 94 | 95 | // Git status colors 96 | $modified-color = #578ddf; 97 | $untracked-color = #0fd000; 98 | $confliction-color = #c13d00; 99 | $added-color = #0fd000; 100 | $deleted-color = #616161; 101 | -------------------------------------------------------------------------------- /src/base-theme/styles/accessUrl.styl: -------------------------------------------------------------------------------- 1 | .access-url { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | } 8 | .access-url-container { 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | overflow: auto; 15 | .access-url-panel { 16 | .panel-heading { 17 | height: 28px; 18 | line-height: 28px; 19 | background-color: $pane-item-border-color; 20 | padding: 0 10px; 21 | display: flex; 22 | input { 23 | line-height: 16px; 24 | margin-left: 6px; 25 | vertical-align: top; 26 | margin-top: 3px; 27 | margin-right: 6px; 28 | } 29 | .btn { 30 | margin-left: 6px; 31 | vertical-align: top; 32 | margin-top: 3px; 33 | } 34 | .fa-refresh { 35 | position: absolute; 36 | top: 7px; 37 | right: 10px; 38 | cursor: pointer; 39 | &:hover { 40 | opacity: .5; 41 | } 42 | } 43 | .panel-title { 44 | flex: 1; 45 | .fa { 46 | margin-right: 4px; 47 | } 48 | } 49 | .panel-title-right { 50 | padding-right: 20px; 51 | .opt-label { 52 | cursor: pointer; 53 | .fa { 54 | margin-right: 4px; 55 | &:hover { 56 | opacity: .5; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | .panel-body { 63 | min-width: 280px; 64 | position: absolute; 65 | top: 28px; 66 | bottom: 0; 67 | left: 0; 68 | right: 0; 69 | overflow: auto; 70 | .create-url { 71 | padding-bottom: 20px; 72 | border-bottom: 1px solid $pane-item-background-color; 73 | .url-tip { 74 | padding: 10px; 75 | line-height: 20px; 76 | font-size: 14px; 77 | color: #737373; 78 | } 79 | .url-generate { 80 | display: flex; 81 | .port { 82 | display: flex; 83 | align-items: center; 84 | height: 36px; 85 | padding: 0 5px 0 10px; 86 | margin: 0 10px; 87 | border: 2px solid #404040; 88 | border-radius: 2px; 89 | span { 90 | color: #6e6e6e; 91 | } 92 | input { 93 | width: 70px; 94 | height: 100%; 95 | margin: 0 5px; 96 | border: none; 97 | outline: none; 98 | background-color: transparent; 99 | } 100 | .change { 101 | display: block; 102 | width: 10px; 103 | .fa { 104 | width: 10px; 105 | height: 15px; 106 | line-height: 15px; 107 | cursor: pointer; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | .port-item { 115 | padding: 10px; 116 | border-bottom: 1px solid $pane-item-background-color; 117 | position: relative; 118 | display: flex; 119 | align-items: center; 120 | .qrcode { 121 | font-size: 46px; 122 | margin-right: 8px; 123 | } 124 | .port-content { 125 | flex-grow: 1; 126 | line-height: 14px; 127 | word-break: break-all; 128 | } 129 | .extra { 130 | .fa { 131 | margin-right: 10px; 132 | cursor: pointer; 133 | } 134 | } 135 | .post-item-info { 136 | line-height: 22px; 137 | display: flex; 138 | align-items: center; 139 | padding-top: 4px; 140 | } 141 | .post-item-ttl { 142 | width: 120px; 143 | font-weight: normal; 144 | margin-bottom: 0; 145 | line-height: 26px; 146 | } 147 | .post-item-upgrade { 148 | color: #4377b6; 149 | cursor: pointer; 150 | .fa { 151 | margin-right: 4px; 152 | } 153 | } 154 | } 155 | .qr-container { 156 | display: none; 157 | position: absolute; 158 | background: #FFF 159 | width: 136px; 160 | height: 136px; 161 | border: 4px solid #FFF; 162 | overflow: hidden; 163 | box-shadow: 0 4px 20px 1px rgba(0, 0, 0, .3); 164 | // padding: 5px; 165 | &.show { 166 | display: block; 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/AccessUrl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | var ReactDOM = require('react-dom'); 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import Clipboard from 'lib/clipboard'; 6 | import * as AccessUrlActions from './actions'; 7 | import cx from 'classnames'; 8 | import { global } from './manager'; 9 | import QRCode from 'qrcode.react'; 10 | 11 | const { notify, NOTIFY_TYPE } = global.sdk.Notify; 12 | const Modal = global.sdk.Modal; 13 | const i18n = global.i18n; 14 | 15 | class AccessUrl extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.state = { 19 | isLoading: true, 20 | ticks: 0, 21 | showQR: false, 22 | port: 8080, 23 | } 24 | this.handlePort = this.handlePort.bind(this); 25 | this.handlePortIncrease = this.handlePortIncrease.bind(this); 26 | this.handlePortDecrease = this.handlePortDecrease.bind(this); 27 | this.handleEnterGenerate = this.handleEnterGenerate.bind(this); 28 | this.handleGenerate = this.handleGenerate.bind(this); 29 | } 30 | componentWillMount () { 31 | this.fetch() 32 | } 33 | componentDidMount () { 34 | const clipboard = new Clipboard('.clipboard', { 35 | text: trigger => trigger.parentElement.parentElement.querySelector('.ip-content').getAttribute('href'), 36 | }); 37 | clipboard.on('success', (e) => { 38 | notify({message: `${e.text} ${i18n.get('global.message.copySuccess')}`}); 39 | }); 40 | clipboard.on('error', (e) => { 41 | notify({message: i18n.get('global.message.copyFailed')}); 42 | }); 43 | } 44 | componentWillUnmount () { 45 | if (this.cdInterval) { 46 | clearInterval(this.cdInterval) 47 | this.cdInterval = undefined 48 | } 49 | } 50 | 51 | render() { 52 | const { portList, generateDisabled } = this.props 53 | return ( 54 |
55 |
56 |
57 |
58 |
59 | 60 | {i18n`global.sidebar`} 61 |
62 | 63 |
64 |
65 |
66 |
67 | {i18n`global.description`} 68 | {i18n`global.help`} 69 |
70 |
71 |
72 | 0.0.0.0 : 73 | 74 |
75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 | {portList.length > 0 ? ( 84 | portList.map((port) => { 85 | if (port.ttl === -1) { 86 | return ( 87 | 95 | ) 96 | } else if ((port.ttl-this.state.ticks) > 0) { 97 | return ( 98 | 107 | ) 108 | } else { 109 | return '' 110 | } 111 | }) 112 | ): ''} 113 |
114 |
115 |
{ this.qr = div; }} > 116 |
117 |
118 |
119 |
120 | ) 121 | } 122 | fetch = () => { 123 | this.setState({ 124 | isLoading: true, 125 | }) 126 | const portListPromise = this.props.actions.listPorts() 127 | Promise.all([portListPromise]).then((res) => { 128 | this.setState({ 129 | isLoading: false, 130 | ticks: 0 131 | }) 132 | if (res.length > 0 && !this.cdInterval) { 133 | this.cdInterval = setInterval(this.tick, 1000) 134 | } 135 | }) 136 | } 137 | tick = () => { 138 | this.setState({ 139 | ticks: this.state.ticks + 1 140 | }) 141 | if (this.props.portList.length <= 0 && this.cdInterval) { 142 | clearInterval(this.cdInterval) 143 | this.cdInterval = undefined 144 | } 145 | } 146 | formatTTL = (ttl) => { 147 | var hour, min, sec, text; 148 | min = Math.floor(ttl / 60); 149 | sec = ttl % 60; 150 | hour = Math.floor(min / 60); 151 | min = min % 60; 152 | text = i18n.get('global.expires'); 153 | text += hour < 10 ? "0" + hour + ":" : hour + ":"; 154 | text += min < 10 ? "0" + min + ":" : min + ":"; 155 | text += sec < 10 ? "0" + sec : "" + sec; 156 | return text; 157 | } 158 | 159 | handlePort(e) { 160 | let value = e.target.value; 161 | if (value !== '') { 162 | value = Number(value); 163 | if (Number.isNaN(value) || value > 100000) { 164 | return; 165 | } 166 | } 167 | this.setState({port: value}); 168 | } 169 | 170 | handlePortIncrease() { 171 | if (this.state.port < 10000) { 172 | this.setState((prevState) => ({ 173 | port: prevState.port + 1, 174 | })); 175 | } 176 | } 177 | 178 | handlePortDecrease() { 179 | if (this.state.port > 0) { 180 | this.setState((prevState) => ({ 181 | port: prevState.port - 1, 182 | })); 183 | } 184 | } 185 | 186 | handleEnterGenerate(e) { 187 | if (e.keyCode === 13) { 188 | this.handleGenerate(e); 189 | } 190 | } 191 | 192 | handleGenerate = (e) => { 193 | e.preventDefault() 194 | this.props.actions.createPort({ port: this.state.port}).then((res) => { 195 | this.setState({ 196 | isLoading: false, 197 | ticks: 0 198 | }) 199 | if (res.length > 0 && !this.cdInterval) { 200 | this.cdInterval = setInterval(this.tick, 1000) 201 | } 202 | }) 203 | } 204 | handleRefrash = (e) => { 205 | e.preventDefault() 206 | this.fetch() 207 | } 208 | handleOpenQR = (e, url) => { 209 | e.preventDefault() 210 | ReactDOM.render(, this.qr) 211 | const qrParentRect = this.qr.parentElement.getBoundingClientRect() 212 | const iconRect = e.target.getBoundingClientRect() 213 | const left = iconRect.left - qrParentRect.left + iconRect.width + 6 214 | const top = iconRect.top - qrParentRect.top 215 | this.qr.style.left = left + 'px' 216 | this.qr.style.top = top + 'px' 217 | this.setState({ 218 | showQR: true 219 | }) 220 | } 221 | handleCloseQR = (e) => { 222 | e.preventDefault() 223 | this.setState({ 224 | showQR: false 225 | }) 226 | } 227 | handleDelete = async (port) => { 228 | var confirmed = await Modal.showModal('Confirm', { 229 | header: i18n`global.handleDelete.header`, 230 | message: i18n`global.handleDelete.message${{port}}`, 231 | okText: i18n`global.handleDelete.okText` 232 | }) 233 | Modal.dismissModal() 234 | if (confirmed) { 235 | this.props.actions.deletePort({ port }) 236 | } 237 | } 238 | handlePermanent = (e, port) => { 239 | e.preventDefault() 240 | if (global.sdk.config.userProfile.vip >= 4) { 241 | this.props.actions.savePort({ port }) 242 | } else { 243 | notify({ 244 | notifyType: NOTIFY_TYPE.INFO, 245 | message: {i18n`global.message.memberInfo`}, 246 | dismissAfter: 10000, 247 | }); 248 | } 249 | } 250 | } 251 | 252 | const PortItem = ({ node, handleOpenQR, handleCloseQR, handleDelete, handlePermanent, ttl }) => { 253 | const ip = `0.0.0.0:${node.port}`; 254 | let ttlDom = ''; 255 | if (ttl === -1) { 256 | ttlDom =
{i18n`global.neverExpires`}
257 | } else { 258 | ttlDom =
259 | 262 | handlePermanent(e, node.port)}> 263 | 264 | {i18n`global.permanent`} 265 | 266 |
267 | } 268 | return ( 269 |
270 |
271 | handleOpenQR(e, node.url)} onMouseLeave={handleCloseQR}> 272 |
273 |
274 | {ip} 275 | {ttlDom} 276 |
277 |
278 | 279 | 280 |
281 |
282 | ) 283 | } 284 | 285 | const mapStateToProps = (state) => { 286 | const { 287 | local: { 288 | generateDisabled, portList = [] 289 | } 290 | } = state 291 | return ({ 292 | generateDisabled, 293 | portList 294 | }); 295 | }; 296 | export default connect(mapStateToProps, dispatch => ({ 297 | actions: bindActionCreators(AccessUrlActions, dispatch) 298 | }))(AccessUrl); 299 | --------------------------------------------------------------------------------