Merge pull request #1133 from tgstation/WebPanel

Adds webpanel v1.0.1 to the project
This commit is contained in:
Jordan Brown
2020-10-12 21:51:55 -04:00
committed by GitHub
21 changed files with 120 additions and 17442 deletions
+3 -3
View File
@@ -20,15 +20,15 @@ build/**
!build/tgs.png
!build/stylecop.json
!build/Version.props
!build/ControlPanelVersion.props
docs
src/DMAPI
src/Tgstation.Server.Host/ClientApp/build
src/Tgstation.Server.Host/ClientApp/node_modules
src/Tgstation.Server.Host/ClientApp
src/Tgstation.Server.Host/wwwroot
src/Tgstation.Server.Host/appsettings.Development.json
src/Tgstation.Server.Host/tgs.bat
src/Tgstation.Server.Host/tgs.sh
src/Tgstation.Server.Host.Client
src/Tgstation.Server.Client
src/Tgstation.Server.Host.Service
tests
tools
+1 -2
View File
@@ -16,8 +16,7 @@ artifacts/
/tests/DMAPI/travistester.dmb
/src/Tgstation.Server.Host/appsettings.*.json
/src/Tgstation.Server.Host/wwwroot
/src/Tgstation.Server.Host/ClientApp/node_modules
/src/Tgstation.Server.Host/ClientApp/build
/src/Tgstation.Server.Host/ClientApp
/tools/ReleaseNotes/release_notes.md
/tools/ReleaseNotes/Properties/launchSettings.json
release_notes.md
+6
View File
@@ -0,0 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- This is in it's own file to help incremental building -->
<TgsControlPanelVersion>1.0.1</TgsControlPanelVersion>
</PropertyGroup>
</Project>
+5 -2
View File
@@ -22,13 +22,16 @@ ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
WORKDIR /repo/build
COPY build/Version.props Version.props
COPY build/ControlPanelVersion.props ControlPanelVersion.props
WORKDIR /repo/src/Tgstation.Server.Host
COPY src/Tgstation.Server.Host/ClientApp ClientApp
COPY src/Tgstation.Server.Host/Tgstation.Server.Host.csproj ./
# I cant figure out how to run npm as non root so eh
RUN npm set unsafe-perm true
RUN dotnet msbuild -target:NpmBuild
RUN npm set unsafe-perm false
WORKDIR /repo
@@ -61,7 +64,7 @@ RUN apt-get update \
gcc-multilib \
gdb \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 5000
ENV General__ValidInstancePaths__0 /tgs4_instances
+4 -3
View File
@@ -1,13 +1,14 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<Import Project="ControlPanelVersion.props" />
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.5.4</TgsCoreVersion>
<TgsConfigVersion>2.1.0</TgsConfigVersion>
<TgsApiVersion>7.3.2</TgsApiVersion>
<TgsClientVersion>8.3.2</TgsClientVersion>
<TgsDmapiVersion>5.2.6</TgsDmapiVersion>
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
</PropertyGroup>
</Project>
@@ -1,3 +0,0 @@
# Web Control Panel Code
This code is responsible for including the selected [tgstation-server-control-panel](https://github.com/tgstation/tgstation-server-control-panel) version with the build.
File diff suppressed because it is too large Load Diff
@@ -1,30 +0,0 @@
{
"private": true,
"dependencies": {
"react-dom": "^16.13.1",
"tgstation-server-control-panel": "0.4.0"
},
"optionalDependencies": {
"fsevents": "1.2.9"
},
"_comment": "fsevent@1.2.9 is locked in to prevent broken builds on windows for v1.2.11",
"scripts": {
"msbuild": "npm run clean && react-scripts build && cp-cli node_modules/tgstation-server-control-panel/build/public/ build/ && rimraf ../wwwroot && move-cli build ../wwwroot",
"clean": "rimraf build"
},
"devDependencies": {
"@types/node": "^13.13.2",
"@types/react-dom": "^16.9.6",
"cp-cli": "^1.1.2",
"move-cli": "^1.2.1",
"react-scripts": "^3.4.1",
"rimraf": "^3.0.2",
"typescript": "^3.8.3"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
@@ -1,52 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Tgstation Server Control Panel</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -1,5 +0,0 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
@@ -1,22 +0,0 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import ControlPanel from "tgstation-server-control-panel";
import registerServiceWorker from "./registerServiceWorker";
import "./index.css";
const contentElement = document.getElementById("root");
if (!contentElement)
throw new Error("Missing root div, cannot render app!");
const legacyUserLanguageKey = "userLanguage";
const userLang = navigator.language || navigator[legacyUserLanguageKey] || "en-CA";
const serverAddress = window.location.href;
const controlPanel = <ControlPanel serverAddress={serverAddress} locale={userLang} />;
registerServiceWorker();
ReactDOM.render(controlPanel, contentElement);
@@ -1 +0,0 @@
/// <reference types="react-scripts" />
@@ -1,123 +0,0 @@
// tslint:disable:no-console
// tslint:disable:curly
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the 'N+1' visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString()
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service worker. To learn more, visit https://goo.gl/SC7cgQ"
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log("New content is available; please refresh.");
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log("Content is cached for offline use.");
}
}
};
}
};
})
.catch(error => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get("content-type")!.indexOf("javascript") === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
@@ -1,37 +0,0 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": [
"es6",
"dom"
],
"sourceMap": true,
"allowJs": true,
"jsx": "preserve",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"exclude": [
"node_modules",
"build"
],
"include": [
"src"
]
}
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using System;
using System.Net.Mime;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// Controller for the web control panel.
/// </summary>
[Route(Application.ControlPanelRoute)]
public class ControlPanelController : Controller
{
/// <summary>
/// The <see cref="IWebHostEnvironment"/> for the <see cref="ControlPanelController"/>.
/// </summary>
readonly IWebHostEnvironment hostEnvironment;
/// <summary>
/// Initializes a new instance of the <see cref="ControlPanelController"/> <see langword="class"/>.
/// </summary>
/// <param name="hostEnvironment">The value of <see cref="hostEnvironment"/>.</param>
public ControlPanelController(IWebHostEnvironment hostEnvironment)
{
this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
}
/// <summary>
/// Handle a GET request to the control panel route. Route to static files if they exist, otherwise index.html.
/// </summary>
/// <param name="appRoute">The value of the route.</param>
/// <returns>The <see cref="VirtualFileResult"/> to use.</returns>
[Route("{**appRoute}")]
[HttpGet]
public VirtualFileResult Get([FromRoute] string appRoute)
{
var fileInfo = hostEnvironment.WebRootFileProvider.GetFileInfo(appRoute);
if (fileInfo.Exists)
{
var contentTypeProvider = new FileExtensionContentTypeProvider();
if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType))
{
contentType = "application/octet-stream";
}
return File(appRoute, contentType);
}
return File("index.html", MediaTypeNames.Text.Html);
}
}
}
@@ -7,7 +7,6 @@ using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Linq;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -100,7 +99,7 @@ namespace Tgstation.Server.Host.Controllers
authenticationContextFactory,
logger,
(browserResolver ?? throw new ArgumentNullException(nameof(browserResolver))).Browser.Type != BrowserType.Generic
&& (controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Enable)
&& !(controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Enable)
{
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
@@ -140,8 +139,8 @@ namespace Tgstation.Server.Host.Controllers
// if we are using a browser and the control panel, soft redirect to the app page
if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
{
Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
return File("~/index.html", MediaTypeNames.Text.Html);
Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), redirecting to control panel...", browserResolver.UserAgent);
return Redirect(Application.ControlPanelRoute);
}
return ApiHeaders == null ? HeadersIssue() : Unauthorized();
+11 -1
View File
@@ -47,6 +47,11 @@ namespace Tgstation.Server.Host.Core
#pragma warning disable CA1506
sealed class Application : SetupApplication
{
/// <summary>
/// Route to the web control panel.
/// </summary>
public const string ControlPanelRoute = "/app";
/// <summary>
/// The <see cref="IWebHostEnvironment"/> for the <see cref="Application"/>.
/// </summary>
@@ -428,7 +433,12 @@ namespace Tgstation.Server.Host.Core
if (controlPanelConfiguration.Enable)
{
logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
applicationBuilder.UseStaticFiles();
applicationBuilder.UseFileServer(new FileServerOptions
{
RequestPath = ControlPanelRoute,
EnableDefaultFiles = true,
EnableDirectoryBrowsing = false
});
}
else
logger.LogDebug("Web control panel disabled!");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../../build/Version.props" />
<PropertyGroup>
@@ -17,25 +17,28 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<PropertyGroup>
<NodeModulesFolder>ClientApp/node_modules</NodeModulesFolder>
<NpmInstallStampFile>ClientApp/node_modules/.install-stamp</NpmInstallStampFile>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>
<Target Name="NpmInstall" Inputs="ClientApp/package-lock.json" Outputs="$(NpmInstallStampFile)">
<Target Name="ClientInstall" Inputs="../../build/ControlPanelVersion.props" Outputs="$(NpmInstallStampFile)">
<Message Text="Pulling web control panel..." Importance="high" />
<RemoveDir Directories="ClientApp" />
<Exec Command="git clone https://github.com/tgstation/tgstation-server-control-panel --branch v$(TgsControlPanelVersion) --depth 1 ClientApp" />
<Message Text="Restoring npm packages..." Importance="high" />
<Exec WorkingDirectory="ClientApp" Command="npm ci" />
<Touch Files="$(NpmInstallStampFile)" AlwaysCreate="true" />
</Target>
<Target Name="NpmBuild" BeforeTargets="BeforeBuild" DependsOnTargets="NpmInstall" Inputs="@(ClientApp)" Outputs="wwwroot\index.html">
<Target Name="NpmBuild" BeforeTargets="BeforeBuild" DependsOnTargets="ClientInstall" Inputs="@(ClientApp)" Outputs="wwwroot\index.html">
<Message Text="Building web control panel..." Importance="high" />
<Exec WorkingDirectory="ClientApp" Command="npm run msbuild" />
</Target>
<Target Name="NpmClean" AfterTargets="Clean">
<Message Text="Cleaning web control panel..." Importance="high" />
<RemoveDir Directories="wwwroot" />
@@ -109,7 +112,7 @@
<ClientApp Include="ClientApp\src\**\*;ClientApp\public\**\*;ClientApp\tsconfig.json;ClientApp\package.json;ClientApp\package-lock.json" />
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+8 -4
View File
@@ -1,4 +1,4 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
@@ -27,7 +27,7 @@ namespace Tgstation.Server.Tests
var doc = XDocument.Load("../../../../../build/Version.props");
var project = doc.Root;
xmlNamespace = project.GetDefaultNamespace();
versionsPropertyGroup = project.Elements().First();
versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup");
Assert.IsNotNull(versionsPropertyGroup);
}
@@ -103,7 +103,11 @@ namespace Tgstation.Server.Tests
[TestMethod]
public void TestControlPanelVersion()
{
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsControlPanelVersion").Value;
var doc = XDocument.Load("../../../../../build/ControlPanelVersion.props");
var project = doc.Root;
var controlPanelXmlNamespace = project.GetDefaultNamespace();
var controlPanelVersionsPropertyGroup = project.Elements().First(x => x.Name == controlPanelXmlNamespace + "PropertyGroup");
var versionString = controlPanelVersionsPropertyGroup.Element(controlPanelXmlNamespace + "TgsControlPanelVersion").Value;
Assert.IsNotNull(versionString);
Assert.IsTrue(Version.TryParse(versionString, out var expected));
@@ -111,7 +115,7 @@ namespace Tgstation.Server.Tests
dynamic json = JObject.Parse(jsonText);
string cpVersionString = json.dependencies["tgstation-server-control-panel"];
string cpVersionString = json.version;
Assert.IsTrue(Version.TryParse(cpVersionString, out var actual));
Assert.AreEqual(expected, actual);
+1
View File
@@ -26,6 +26,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6FF654E6
ProjectSection(SolutionItems) = preProject
build\analyzers.ruleset = build\analyzers.ruleset
build\BuildDox.ps1 = build\BuildDox.ps1
build\ControlPanelVersion.props = build\ControlPanelVersion.props
build\Dockerfile = build\Dockerfile
build\GenerateMigrations.sh = build\GenerateMigrations.sh
build\OpenApiValidationSettings.json = build\OpenApiValidationSettings.json
+14 -5
View File
@@ -1,4 +1,4 @@
using Octokit;
using Octokit;
using System;
using System.Collections.Generic;
using System.IO;
@@ -34,10 +34,14 @@ namespace ReleaseNotes
var doNotCloseMilestone = args.Length > 1 && args[1].ToUpperInvariant() == "--NO-CLOSE";
string propsPath = "../../../../../build/Version.props";
var propsPath = "../../../../../build/Version.props";
if (args.Length > 1 && !doNotCloseMilestone)
propsPath = args[1];
var controlPanelPropsPath = "../../../../../build/Version.props";
if (args.Length > 2 && !doNotCloseMilestone)
propsPath = args[2];
const string ReleaseNotesEnvVar = "TGS4_RELEASE_NOTES_TOKEN";
var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
if (String.IsNullOrWhiteSpace(githubToken) && !doNotCloseMilestone)
@@ -49,7 +53,7 @@ namespace ReleaseNotes
try
{
var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes"));
if (!String.IsNullOrWhiteSpace(githubToken))
if (!String.IsNullOrWhiteSpace(githubToken))
{
client.Credentials = new Credentials(githubToken);
}
@@ -240,7 +244,12 @@ namespace ReleaseNotes
var doc = XDocument.Load(propsPath);
var project = doc.Root;
var xmlNamespace = project.GetDefaultNamespace();
var versionsPropertyGroup = project.Elements().First();
var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup");
var doc2 = XDocument.Load(controlPanelPropsPath);
var project2 = doc.Root;
var controlPanelXmlNamespace = project.GetDefaultNamespace();
var controlPanelVersionsPropertyGroup = project.Elements().First(x => x.Name == controlPanelXmlNamespace + "PropertyGroup");
var coreVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value);
if(coreVersion != version)
@@ -252,7 +261,7 @@ namespace ReleaseNotes
var apiVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsApiVersion").Value);
var configVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsConfigVersion").Value);
var dmApiVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsDmapiVersion").Value);
var webControlVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsControlPanelVersion").Value);
var webControlVersion = Version.Parse(controlPanelVersionsPropertyGroup.Element(controlPanelXmlNamespace + "TgsControlPanelVersion").Value);
var hostWatchdogVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsHostWatchdogVersion").Value);
if (webControlVersion.Major == 0)