├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE.txt ├── README.md ├── azdevops.webpack.config.js ├── azure-pipelines.yml ├── buildScripts ├── CssVariablesLibrary.js ├── css-variables-loader.js ├── css-variables-processor.js └── cssDefaults.json ├── buildpackage.js ├── docs ├── ServicesPivot.png └── WorkloadsPivot.png ├── package-lock.json ├── package.json ├── scripts └── FailIfDirty.cmd ├── src ├── Contracts │ ├── Contracts.ts │ ├── KubeServiceBase.ts │ └── Types.d.ts ├── Resources.ts ├── WebUI │ ├── Common │ │ ├── ContentReader.scss │ │ ├── ContentReader.tsx │ │ ├── DefaultImageLocation.ts │ │ ├── KubeCardWithTable.scss │ │ ├── KubeCardWithTable.tsx │ │ ├── KubeConsumer.ts │ │ ├── KubeFilterBar.tsx │ │ ├── KubeSummary.scss │ │ ├── KubeSummary.tsx │ │ ├── KubeZeroData.scss │ │ ├── KubeZeroData.tsx │ │ ├── PageTopHeader.tsx │ │ ├── ResourceStatus.tsx │ │ ├── Tags.scss │ │ └── Tags.tsx │ ├── Constants.ts │ ├── FluxCommon │ │ ├── Actions.ts │ │ ├── ActionsCreatorManager.ts │ │ ├── ActionsHubManager.ts │ │ ├── Factory.ts │ │ ├── Store.ts │ │ └── StoreManager.ts │ ├── ImageDetails │ │ ├── ImageDetails.scss │ │ ├── ImageDetails.tsx │ │ ├── ImageDetailsActions.ts │ │ ├── ImageDetailsActionsCreator.ts │ │ └── ImageDetailsStore.ts │ ├── KubeFactory.ts │ ├── Pods │ │ ├── PodContentReader.tsx │ │ ├── PodLog.tsx │ │ ├── PodOverview.scss │ │ ├── PodOverview.tsx │ │ ├── PodYaml.tsx │ │ ├── PodsActions.ts │ │ ├── PodsActionsCreator.ts │ │ ├── PodsDetails.tsx │ │ ├── PodsLeftPanel.scss │ │ ├── PodsLeftPanel.tsx │ │ ├── PodsRightPanel.scss │ │ ├── PodsRightPanel.tsx │ │ ├── PodsStore.ts │ │ ├── PodsTable.scss │ │ ├── PodsTable.tsx │ │ └── Types.d.ts │ ├── RunDetails.scss │ ├── RunDetails.tsx │ ├── Selection │ │ ├── SelectionActionCreator.ts │ │ ├── SelectionActions.ts │ │ └── SelectionStore.ts │ ├── Services │ │ ├── ServiceDetails.scss │ │ ├── ServiceDetails.tsx │ │ ├── ServiceUtils.ts │ │ ├── ServicesActions.ts │ │ ├── ServicesActionsCreator.ts │ │ ├── ServicesFilterBar.tsx │ │ ├── ServicesPivot.scss │ │ ├── ServicesPivot.tsx │ │ ├── ServicesStore.ts │ │ └── ServicesTable.tsx │ ├── Types.d.ts │ ├── Utils.ts │ └── Workloads │ │ ├── DeploymentsTable.scss │ │ ├── DeploymentsTable.tsx │ │ ├── OtherWorkloadsTable.tsx │ │ ├── WorkloadDetails.scss │ │ ├── WorkloadDetails.tsx │ │ ├── WorkloadsActions.ts │ │ ├── WorkloadsActionsCreator.ts │ │ ├── WorkloadsFilterBar.tsx │ │ ├── WorkloadsPivot.scss │ │ ├── WorkloadsPivot.tsx │ │ └── WorkloadsStore.ts ├── img │ ├── zero-data.svg │ ├── zero-results.svg │ └── zero-workloads.svg ├── index.ts └── tsconfig.json ├── tests ├── Contracts │ └── KubeServiceBase.test.ts ├── TestCore.js ├── WebUI │ ├── ActionsCreatorTests │ │ └── ImageDetailsActionsCreator.test.ts │ ├── Components │ │ ├── KubeCardWithTable.test.tsx │ │ ├── ServiceDetails.test.tsx │ │ └── WorkloadDetails.test.tsx │ ├── MockImageService.ts │ ├── MockKubeService.ts │ └── Utils.test.ts ├── tsconfig.json └── webpack.config.js └── webapp.webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | npm-debug.log 7 | node_modules/ 8 | _bin/ 9 | dist/ 10 | dist_tests/ 11 | *.vsix 12 | *.orig 13 | *.tgz 14 | 15 | # User-specific files 16 | *.suo 17 | *.user 18 | *.userosscache 19 | *.sln.docstates 20 | 21 | # User-specific files (MonoDevelop/Xamarin Studio) 22 | *.userprefs 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUNIT 49 | *.VisualState.xml 50 | TestResult.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | **/Properties/launchSettings.json 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_i.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # JustCode is a .NET coding add-in 132 | .JustCode 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Visual Studio code coverage results 145 | *.coverage 146 | *.coveragexml 147 | 148 | # NCrunch 149 | _NCrunch_* 150 | .*crunch*.local.xml 151 | nCrunchTemp_* 152 | 153 | # MightyMoose 154 | *.mm.* 155 | AutoTest.Net/ 156 | 157 | # Web workbench (sass) 158 | .sass-cache/ 159 | 160 | # Installshield output folder 161 | [Ee]xpress/ 162 | 163 | # DocProject is a documentation generator add-in 164 | DocProject/buildhelp/ 165 | DocProject/Help/*.HxT 166 | DocProject/Help/*.HxC 167 | DocProject/Help/*.hhc 168 | DocProject/Help/*.hhk 169 | DocProject/Help/*.hhp 170 | DocProject/Help/Html2 171 | DocProject/Help/html 172 | 173 | # Click-Once directory 174 | publish/ 175 | 176 | # Publish Web Output 177 | *.[Pp]ublish.xml 178 | *.azurePubxml 179 | # Note: Comment the next line if you want to checkin your web deploy settings, 180 | # but database connection strings (with potential passwords) will be unencrypted 181 | *.pubxml 182 | *.publishproj 183 | 184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 185 | # checkin your Azure Web App publish settings, but sensitive information contained 186 | # in these scripts will be unencrypted 187 | PublishScripts/ 188 | 189 | # NuGet Packages 190 | *.nupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | 265 | # Microsoft Fakes 266 | FakesAssemblies/ 267 | 268 | # GhostDoc plugin setting file 269 | *.GhostDoc.xml 270 | 271 | # Node.js Tools for Visual Studio 272 | .ntvs_analysis.dat 273 | node_modules/ 274 | 275 | # Visual Studio 6 build log 276 | *.plg 277 | 278 | # Visual Studio 6 workspace options file 279 | *.opt 280 | 281 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 282 | *.vbw 283 | 284 | # Visual Studio LightSwitch build output 285 | **/*.HTMLClient/GeneratedArtifacts 286 | **/*.DesktopClient/GeneratedArtifacts 287 | **/*.DesktopClient/ModelManifest.xml 288 | **/*.Server/GeneratedArtifacts 289 | **/*.Server/ModelManifest.xml 290 | _Pvt_Extensions 291 | 292 | # Paket dependency manager 293 | .paket/paket.exe 294 | paket-files/ 295 | 296 | # FAKE - F# Make 297 | .fake/ 298 | 299 | # JetBrains Rider 300 | .idea/ 301 | *.sln.iml 302 | 303 | # CodeRush 304 | .cr/ 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | azpipelines-kubernetesUI 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | The MIT License (MIT) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure Pipelines Kubernetes UI 3 | 4 | ## Overview 5 | 6 | This repo contains React based UI view of a kubernetes cluster and will be used in azure devops pipelines. This UI is hostable outside of azure pipelines product and does not require the UI web server to be running inside the kubernetes cluster. 7 | 8 | This repo consists of 9 | 1. Contracts: The `IKubeService` provides the interface that needs to be implemented inorder to fetch data needed for the UI 10 | 2. WebUI: It contains the components that make up the UI 11 | 12 | ## Usage of Kubernetes UI within Azure DevOps 13 | 14 | This Web UI will be integrated into Azure DevOps as an extension and will be available by default in your Azure DevOps accounts going forward. The repo for the extension is at [azPipeline-KubernetesUI-devopsExtension](https://github.com/Microsoft/azPipeline-KubernetesUI-devopsExtension). 15 | 16 | ## Host the Kubernetes UI within your Web Application 17 | 18 | You can also host the UI outside of Azure DevOps. Refer to the [azpipelines-kubernetesUI-WebApp](https://github.com/Microsoft/azpipelines-kubernetesUI-WebApp) repository as a working reference on how to host the Kubernetes UI in a stand-alone web app. It also has a custom implementation of `IKubeService` to fetch the required Kubernets objects. 19 | 20 | ![Cluster workloads page UI](docs/WorkloadsPivot.png) 21 | 22 | ![Cluster services page UI](docs/ServicesPivot.png) 23 | 24 | ## Prerequisites: Node and Npm 25 | 26 | **Windows and Mac OSX**: Download and install node from [nodejs.org](http://nodejs.org/) 27 | 28 | **Linux**: Install [using package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager) 29 | 30 | From a terminal ensure at least node 8.4 and npm 5.3: 31 | 32 | ```bash 33 | $ node -v && npm -v 34 | v8.4.0 35 | 5.3.0 36 | ``` 37 | 38 | To install npm separately: 39 | 40 | ``` 41 | [sudo] npm install npm@5 -g 42 | npm -v 43 | 5.6.0 44 | ``` 45 | 46 | Note: On windows if it's still returning npm 2.x run `where npm`. Notice hits in program files. Rename those two npm files and the 5.6.0 in AppData will win. 47 | 48 | ## Build 49 | 50 | npm install 51 | npm run build 52 | 53 | ## Test 54 | To clean test binaries, build test binaries and run tests 55 | 56 | npm run ctest 57 | 58 | To build test binaries and run tests 59 | 60 | npm test 61 | 62 | ## Dependencies 63 | 64 | This repository depends on the following packages: 65 | 66 | - [azure-devops-ui](https://www.npmjs.com/package/azure-devops-ui): UI library containing the React components used in the Azure DevOps web UI. 67 | - [@kubernetes/client-node](https://github.com/kubernetes-client/javascript): The Javascript clients for Kubernetes implemented in typescript. 68 | - [office-ui-fabric-react](https://github.com/OfficeDev/office-ui-fabric-react): React components for building experiences for Office and Office 365 69 | 70 | Some external dependencies: 71 | - `React` - Is used to render the UI in the samples, and is a dependency of `azure-devops-ui`. 72 | - `TypeScript` - Samples are written in TypeScript and complied to JavaScript 73 | - `SASS` - Extension samples are styled using SASS (which is compiled to CSS and delivered in webpack js bundles). 74 | - `webpack` - Is used to gather dependencies into a single javascript bundle for each sample. 75 | - `jest` - Is used as unit test framework. 76 | - `enzyme` - Test utility to test react components. 77 | 78 | ## Contributing 79 | 80 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 81 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 82 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 83 | 84 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 85 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 86 | provided by the bot. You will only need to do this once across all repos using our CLA. 87 | 88 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 89 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 90 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 91 | -------------------------------------------------------------------------------- /azdevops.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | const CircularDependencyPlugin = require("circular-dependency-plugin"); 5 | 6 | module.exports = { 7 | entry: { 8 | "azdevops-kube-summary": "./src/index.ts", 9 | "azdevops-kube-summary.min": "./src/index.ts" 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, "_bin/azDevOpsPackage/_bundles"), 13 | filename: "[name].js", 14 | libraryTarget: "umd", 15 | library: "azdevops-kube-summary", 16 | umdNamedDefine: true 17 | }, 18 | resolve: { 19 | extensions: [".ts", ".tsx", ".js"] 20 | }, 21 | devtool: "source-map", 22 | optimization: { 23 | minimizer: [ 24 | new TerserPlugin({ 25 | sourceMap: true, 26 | include: /\.min\.js$/, 27 | parallel: 4 28 | }) 29 | ] 30 | }, 31 | plugins: [ 32 | new CircularDependencyPlugin({ 33 | onStart({ compilation }) { 34 | // `onStart` is called before the cycle detection starts 35 | console.log("Detecting webpack modules cycles -- start."); 36 | }, 37 | onDetected({ module: webpackModuleRecord, paths, compilation }) { 38 | // `paths` will be an Array of the relative module paths that make up the cycle 39 | // `module` will be the module record generated by webpack that caused the cycle 40 | const cyclePaths = paths.join(' -> '); 41 | compilation.errors.push(new Error(cyclePaths)) 42 | console.error("Cycle detected: " + cyclePaths); 43 | }, 44 | // `onEnd` is called before the cycle detection ends 45 | onEnd({ compilation }) { 46 | console.log("Done detecting webpack modules cycles."); 47 | }, 48 | failOnError: true, 49 | cwd: process.cwd() 50 | }) 51 | ], 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.tsx?$/, 56 | loader: "ts-loader" 57 | }, 58 | { 59 | test: /\.scss$/, 60 | use: ["style-loader", "css-loader", "./buildScripts/css-variables-loader", "sass-loader"] 61 | }, 62 | { 63 | test: /\.css$/, 64 | use: ["style-loader", "css-loader"], 65 | }, 66 | { 67 | test: /\.woff$/, 68 | use: [{ 69 | loader: "base64-inline-loader" 70 | }] 71 | }, 72 | { 73 | test: /\.html$/, 74 | loader: "file-loader" 75 | }, 76 | { test: /\.(png|jpg|svg)$/, loader: "file-loader" }, 77 | ] 78 | }, 79 | node: { 80 | fs: "empty", 81 | tls: "mock", 82 | child_process: "empty", 83 | net: "empty" 84 | }, 85 | externals: [ 86 | function(context, request, callback) { 87 | const azDevOpsUIPrefix = "azure-devops-ui"; 88 | const azDevOpsUICorePrefix = "azure-devops-ui/Core"; 89 | const propsPostfix = ".Props"; 90 | 91 | if (request === "react" || request === "react-dom") { 92 | return callback(null, request); 93 | } 94 | else if (request.startsWith(azDevOpsUICorePrefix)) { 95 | // replace "azure-devops-ui/Core" with VSS/Core 96 | return callback(null, "VSS" + request.substr(azDevOpsUIPrefix.length)); 97 | } 98 | else if (request.startsWith(azDevOpsUIPrefix) && request.endsWith(propsPostfix)) { 99 | // replace "azure-devops-ui" with VSSUI and drop ".Props" 100 | return callback(null, "VSSUI" + request.substr(azDevOpsUIPrefix.length, request.length - azDevOpsUIPrefix.length - propsPostfix.length)); 101 | } 102 | else if (request.startsWith(azDevOpsUIPrefix)) { 103 | // replace "azure-devops-ui" with VSSUI 104 | return callback(null, "VSSUI" + request.substr(azDevOpsUIPrefix.length)); 105 | } 106 | else { 107 | // return unchanged 108 | return callback(); 109 | } 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | name: k8sUI_$(SourceBranchName)_$(Date:MMMd)$(Rev:.r) 2 | 3 | # PR trigger 4 | pr: 5 | - master 6 | 7 | # CI trigger 8 | trigger: 9 | - master 10 | 11 | jobs: 12 | - job: Build 13 | timeoutInMinutes: 20 14 | cancelTimeoutInMinutes: 2 15 | pool: 16 | name: 'Hosted macOS' 17 | 18 | steps: 19 | - task: NodeTool@0 20 | displayName: 'Use Node version' 21 | inputs: 22 | versionSpec: '8.5' 23 | 24 | - task: Npm@1 25 | displayName: 'npm install' 26 | inputs: 27 | command: install 28 | 29 | - task: Npm@1 30 | displayName: 'Build source' 31 | inputs: 32 | command: custom 33 | customCommand: 'run build-all' 34 | 35 | - task: Npm@1 36 | displayName: 'Run UTs' 37 | inputs: 38 | command: custom 39 | customCommand: 'run test' 40 | 41 | - task: PublishTestResults@2 42 | condition: succeededOrFailed() 43 | displayName: 'Publish L0 UTs' 44 | inputs: 45 | testRunner: JUnit 46 | testResultsFiles: '**/jest-l0-uts.xml' 47 | testRunTitle: 'L0 UTs' 48 | 49 | # publish dist for master branch only 50 | - ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/master') }}: 51 | - task: ArchiveFiles@2 52 | displayName: 'Archive dist folder' 53 | inputs: 54 | rootFolderOrFile: '$(System.DefaultWorkingDirectory)/dist' 55 | archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildNumber).zip' 56 | 57 | - task: ArchiveFiles@2 58 | displayName: 'Archive License to dist folder' 59 | inputs: 60 | rootFolderOrFile: LICENSE.txt 61 | includeRootFolder: false 62 | archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildNumber).zip' 63 | replaceExistingArchive: false 64 | 65 | - task: ArchiveFiles@2 66 | displayName: 'Archive README to dist folder' 67 | inputs: 68 | rootFolderOrFile: README.md 69 | includeRootFolder: false 70 | archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildNumber).zip' 71 | replaceExistingArchive: false 72 | 73 | - task: ArchiveFiles@2 74 | displayName: 'Archive package to dist folder' 75 | inputs: 76 | rootFolderOrFile: package.json 77 | includeRootFolder: false 78 | archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildNumber).zip' 79 | replaceExistingArchive: false 80 | 81 | - task: PublishBuildArtifacts@1 82 | displayName: 'Publish Artifact: dist' 83 | inputs: 84 | ArtifactName: dist 85 | -------------------------------------------------------------------------------- /buildScripts/CssVariablesLibrary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This utility is used to post-process CSS containing CSS variables so that a default (non-themed) version works 3 | * on browsers that don't support CSS variables (IE 11). 4 | * 5 | * This requires a JSON file containing the default values for all possible CSS variables. It will 6 | * find any var(--XXX) values in CSS and emit 2 styles, one with the default value, then a second 7 | * (higher precedence) that uses the CSS variable. 8 | * 9 | * So the following: 10 | * 11 | * .button { 12 | * color: var(--button-color); 13 | * } 14 | * 15 | * becomes: 16 | * 17 | * .button { 18 | * color: #ccc; 19 | * color: var(--button-color, #ccc); 20 | * } 21 | * 22 | * given a defaults file like: 23 | * 24 | * { 25 | * "button-color": "#ccc" 26 | * } 27 | */ 28 | 29 | const fs = require("fs"); 30 | const path = require("path"); 31 | 32 | const varString = "var(--"; 33 | const borderAttributes = { 34 | "border": true, 35 | "border-top": true, 36 | "border-right": true, 37 | "border-bottom": true, 38 | "border-left": true 39 | }; 40 | 41 | function getVarReplacement(line, defaults, start, varReplacements) { 42 | const indexOfNextVar = line.indexOf(varString, start); 43 | 44 | if (indexOfNextVar === -1) return; 45 | // Need to find the end of the var function. It will be a ), but it may not be the first one. 46 | // for example: var(--secondary-text, rgba(0, 0, 0, 0.55)) 47 | let openParens = 0; 48 | let indexOfEndOfVar = -1; 49 | for (let j = indexOfNextVar + varString.length; j < line.length; j++) { 50 | let check = line[j]; 51 | 52 | // Keep track of opened parens. If we hit a closing paren and there were no open parens, 53 | // we found the end of the var. 54 | if (check === "(") { 55 | openParens++; 56 | } else if (check === ")") { 57 | if (openParens === 0) { 58 | indexOfEndOfVar = j; 59 | break; 60 | } else { 61 | openParens--; 62 | } 63 | } 64 | } 65 | 66 | if (indexOfEndOfVar === -1) { 67 | console.error(`Did not find closing bracket for var function: ${line}}`); 68 | return; 69 | } 70 | 71 | const varInput = line.substr(indexOfNextVar + 4, indexOfEndOfVar - indexOfNextVar - 4); 72 | const varInputs = varInput.split(","); 73 | let themeEntry = varInputs[0].trim(); 74 | const replacementVar = defaults[themeEntry.substr(2)]; 75 | 76 | if (!replacementVar) { 77 | console.error(`Did not find replacement variable for: '${themeEntry}'. Line: ${line}`); 78 | } 79 | 80 | varReplacements.push({ 81 | string: line.substr(indexOfNextVar, indexOfEndOfVar - indexOfNextVar + 1), 82 | replacement: replacementVar, 83 | defaultSet: varInputs.length === 2, 84 | themeEntry: themeEntry 85 | }); 86 | 87 | getVarReplacement(line, defaults, indexOfEndOfVar, varReplacements); 88 | } 89 | 90 | function trimEnd(val) { 91 | // Shim trimEnd for older versions of node 92 | return ("x" + val).trim().substr(1); 93 | } 94 | 95 | function processCssLine(line, defaults, outputLines) { 96 | let writeOriginalLine = true; 97 | 98 | let duplicateLine = line; 99 | let defaultsLine = line; 100 | // Look to see if line might have an attribute 101 | let indexOfAttribute = line.indexOf(":"); 102 | 103 | // If there is an attribute, then look for a var function. var function will start witht var(-- 104 | if (indexOfAttribute > -1) { 105 | const indexOfFirstVar = line.indexOf(varString, indexOfAttribute); 106 | if (indexOfFirstVar > -1) { 107 | // There is a var function so we will be writing a duplicate attribute. 108 | const attribute = line.substr(0, indexOfAttribute); 109 | 110 | // Special-case handling for the "border" attribute for a bug in Safari where it doesn't support 111 | // rgba with a variable unless it is at the start of the border attribute. We will pull this out 112 | // into two separate statements. A border: with the width and fill, and a border-color with the 113 | // rgba/variable combo. 114 | // 115 | // Safari bug: https://bugs.webkit.org/show_bug.cgi?id=185940 116 | // 117 | // Example: 118 | // border: 1px solid rgba(var(--palette-color), 0.1); 119 | // 120 | // becomes: 121 | // border: 1px solid; 122 | // border-color: rgba(var(--palette-color), 0.1); 123 | // 124 | // then gets each of these lines re-processed 125 | 126 | let rgbaIndex; 127 | if (borderAttributes[attribute.trim()] && (rgbaIndex = line.indexOf(" rgba(", indexOfAttribute + 1)) !== -1) { 128 | let preRgba = line.substr(indexOfAttribute + 1, rgbaIndex - indexOfAttribute - 1); 129 | if (preRgba) { 130 | processCssLine(`${attribute}: ${preRgba};`, defaults, outputLines); 131 | processCssLine(`${attribute}-color: ${line.substr(rgbaIndex + 1)}`, defaults, outputLines); 132 | return; 133 | } 134 | } 135 | let replacements = []; 136 | getVarReplacement(line, defaults, indexOfFirstVar, replacements); 137 | 138 | for (let i = 0; i < replacements.length; i++) { 139 | let varReplacement = replacements[i]; 140 | let string = varReplacement.string; 141 | let replacement = varReplacement.replacement; 142 | let defaultsReplacement = `var(${varReplacement.themeEntry},${varReplacement.replacement})`; 143 | 144 | duplicateLine = duplicateLine.replace(string, replacement); 145 | 146 | // If no default was set, then write the line back out with a default. 147 | if (!varReplacement.defaultSet) { 148 | writeOriginalLine = false; 149 | defaultsLine = defaultsLine.replace(string, defaultsReplacement); 150 | } 151 | } 152 | 153 | while (duplicateLine.endsWith("}")) { 154 | duplicateLine = trimEnd(duplicateLine.substr(0, duplicateLine.length - 1)); 155 | } 156 | 157 | outputLines.push(duplicateLine); 158 | } 159 | } 160 | 161 | if (writeOriginalLine) { 162 | outputLines.push(line); 163 | } else { 164 | outputLines.push(defaultsLine); 165 | } 166 | } 167 | 168 | function processCssContent(content, defaults) { 169 | const originalLines = content.split("\n"); 170 | const outputLines = []; 171 | 172 | originalLines.forEach(line => { 173 | processCssLine(line, defaults, outputLines); 174 | }); 175 | 176 | return outputLines.join("\n"); 177 | } 178 | 179 | function loadDefaultValues(defaultFilePaths) { 180 | let defaultsPaths = [path.join(__dirname, "cssDefaults.json")]; 181 | 182 | if (defaultFilePaths) { 183 | defaultsPaths = defaultsPaths.concat(defaultFilePaths); 184 | } 185 | 186 | let defaults = {}; 187 | defaultsPaths.forEach(file => { 188 | defaults = { ...defaults, ...JSON.parse(fs.readFileSync(file, "utf8")) }; 189 | }); 190 | 191 | return defaults; 192 | } 193 | 194 | module.exports = { 195 | processCssContent, 196 | loadDefaultValues 197 | }; 198 | -------------------------------------------------------------------------------- /buildScripts/css-variables-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This webpack loader is used to post-process CSS containing CSS variables so that a default (non-themed) version works 3 | * on browsers that don't support CSS variables (IE 11). 4 | * 5 | * This will find any var(--XXX) values in CSS and emit 2 styles, one with the default value, then a second 6 | * (higher precedence) that uses the CSS variable. 7 | * 8 | * So the following: 9 | * 10 | * .button { 11 | * color: var(--button-color); 12 | * } 13 | * 14 | * becomes: 15 | * 16 | * .button { 17 | * color: #ccc; 18 | * color: var(--button-color, #ccc); 19 | * } 20 | * 21 | * given a defaults file like: 22 | * 23 | * { 24 | * "button-color": "#ccc" 25 | * } 26 | */ 27 | 28 | const utils = require("loader-utils"); 29 | const cssVariables = require("./CssVariablesLibrary.js"); 30 | 31 | module.exports = function(content) { 32 | this.cacheable(); 33 | 34 | const options = utils.getOptions(this); 35 | 36 | const defaults = cssVariables.loadDefaultValues(options && options.defaultsPaths); 37 | const result = cssVariables.processCssContent(content, defaults); 38 | 39 | this.value = result; 40 | return result; 41 | } -------------------------------------------------------------------------------- /buildScripts/css-variables-processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This build script will traverse all CSS files in a directory and post-process them such that any CSS variables 3 | * are emitted in a way that a default (non-themed) version works on browsers that don't support CSS variables (IE 11). 4 | * 5 | * This requires a JSON file containing the default values for all possible CSS variables. It will 6 | * find any var(--XXX) values in CSS and emit 2 styles, one with the default value, then a second 7 | * (higher precedence) that uses the CSS variable. 8 | * 9 | * So the following: 10 | * 11 | * .button { 12 | * color: var(--button-color); 13 | * } 14 | * 15 | * becomes: 16 | * 17 | * .button { 18 | * color: #ccc; 19 | * color: var(--button-color, #ccc); 20 | * } 21 | * 22 | * given a defaults file like: 23 | * 24 | * { 25 | * "button-color": "#ccc" 26 | * } 27 | * 28 | * Required argument: 29 | * --rootFolder: Folder under which to traverse all .css files 30 | * 31 | * Optional argument: 32 | * --defaults: List of JSON files (separated by semi-colon) to use as default values 33 | */ 34 | 35 | const fs = require("fs"); 36 | const glob = require("glob"); 37 | const parseArgs = require("minimist"); 38 | const cssVariables = require("./CssVariablesLibrary.js"); 39 | 40 | function processCssFile(file, defaults) { 41 | const originalContent = fs.readFileSync(file, "utf8"); 42 | const newContent = cssVariables.processCssContent(originalContent, defaults); 43 | fs.writeFileSync(file, newContent); 44 | } 45 | 46 | async function processCss(rootFolder, defaultsPaths) { 47 | 48 | let defaults = cssVariables.loadDefaultValues(defaultsPaths); 49 | 50 | const files = await new Promise((resolve, reject) => { 51 | glob(rootFolder + "/**/*.css", (err, files) => { 52 | if (err) { 53 | reject(err); 54 | } else { 55 | resolve(files); 56 | } 57 | }); 58 | }); 59 | 60 | for (const file of files) { 61 | processCssFile(file, defaults); 62 | } 63 | } 64 | 65 | const parsedArgs = parseArgs(process.argv.slice(2)); 66 | const rootFolder = parsedArgs.root || "."; 67 | const defaultsPaths = parsedArgs.defaults; 68 | 69 | processCss(rootFolder, defaultsPaths ? defaultsPaths.split(";") : undefined); -------------------------------------------------------------------------------- /buildScripts/cssDefaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "palette-primary-darken-6": "rgba(0, 103, 181, 1)", 3 | "palette-primary-darken-10": "rgba(0, 91, 161, 1)", 4 | "palette-primary-shade-30": "0, 69, 120", 5 | "palette-primary-shade-20": "0, 90, 158", 6 | "palette-primary-shade-10": "16, 110, 190", 7 | "palette-primary": "0, 120, 212", 8 | "palette-primary-tint-10": "43, 136, 216", 9 | "palette-primary-tint-20": "199, 224, 244", 10 | "palette-primary-tint-30": "222, 236, 249", 11 | "palette-primary-tint-40": "239, 246, 252", 12 | 13 | "palette-neutral-100": "0, 0, 0", 14 | "palette-neutral-80": "51, 51, 51", 15 | "palette-neutral-70": "76, 76, 76", 16 | "palette-neutral-60": "102, 102, 102", 17 | "palette-neutral-30": "166, 166, 166", 18 | "palette-neutral-20": "200, 200, 200", 19 | "palette-neutral-10": "218, 218, 218", 20 | "palette-neutral-8": "234, 234, 234", 21 | "palette-neutral-6": "239, 239, 239", 22 | "palette-neutral-4": "244, 244, 244", 23 | "palette-neutral-2": "248, 248, 248", 24 | "palette-neutral-0": "255, 255, 255", 25 | 26 | "palette-error": "rgba(232, 17, 35, 1)", 27 | "palette-error-6": "rgba(203, 15, 31, 1)", 28 | "palette-error-10": "rgba(184, 14, 28, 1)", 29 | 30 | "palette-black-alpha-0": "rgba(0, 0, 0, 0)", 31 | "palette-black-alpha-2": "rgba(0, 0, 0, 0.02)", 32 | "palette-black-alpha-4": "rgba(0, 0, 0, 0.04)", 33 | "palette-black-alpha-6": "rgba(0, 0, 0, 0.06)", 34 | "palette-black-alpha-8": "rgba(0, 0, 0, 0.08)", 35 | "palette-black-alpha-10": "rgba(0, 0, 0, 0.10)", 36 | "palette-black-alpha-20": "rgba(0, 0, 0, 0.20)", 37 | "palette-black-alpha-30": "rgba(0, 0, 0, 0.30)", 38 | "palette-black-alpha-60": "rgba(0, 0, 0, 0.60)", 39 | "palette-black-alpha-70": "rgba(0, 0, 0, 0.70)", 40 | "palette-black-alpha-80": "rgba(0, 0, 0, 0.80)", 41 | "palette-black-alpha-100": "rgba(0, 0, 0, 1)", 42 | 43 | "palette-white-alpha-0": "rgba(255, 255, 255, 0)", 44 | "palette-white-alpha-2": "rgba(255, 255, 255, 0.02)", 45 | "palette-white-alpha-4": "rgba(255, 255, 255, 0.04)", 46 | "palette-white-alpha-6": "rgba(255, 255, 255, 0.08)", 47 | "palette-white-alpha-8": "rgba(255, 255, 255, 0.12)", 48 | "palette-white-alpha-10": "rgba(255, 255, 255, 0.18)", 49 | "palette-white-alpha-20": "rgba(255, 255, 255, 0.29)", 50 | "palette-white-alpha-30": "rgba(255, 255, 255, 0.40)", 51 | "palette-white-alpha-60": "rgba(255, 255, 255, 0.57)", 52 | "palette-white-alpha-70": "rgba(255, 255, 255, 0.7)", 53 | "palette-white-alpha-80": "rgba(255, 255, 255, 0.86)", 54 | "palette-white-alpha-100": "rgba(255, 255, 255, 1)", 55 | 56 | "palette-accent1-light": "249, 235, 235", 57 | "palette-accent1": "218, 10, 0", 58 | "palette-accent1-dark": "168, 0, 0", 59 | 60 | "palette-accent2-light": "223, 246, 221", 61 | "palette-accent2": "186, 216, 10", 62 | "palette-accent2-dark": "16, 124, 16", 63 | 64 | "palette-accent3-light": "255, 244, 206", 65 | "palette-accent3": "248, 168, 0", 66 | "palette-accent3-dark": "220, 182, 122", 67 | 68 | "communication-foreground": "rgba(0, 120, 212, 1)", 69 | "communication-background": "rgba(0, 120, 212, 1)", 70 | 71 | "status-error-foreground": "rgba(205, 74, 69, 1)", 72 | "status-error-background": "rgba(249, 235, 235, 1)", 73 | "status-error-text": "rgba(218, 10, 0, 1)", 74 | "status-error-strong": "rgba(168, 0, 0, 1)", 75 | 76 | "status-info-foreground": "rgba(0, 120, 212, 1)", 77 | "status-info-background": "rgba(0, 120, 212, 1)", 78 | 79 | "status-success-foreground": "rgba(85, 163, 98, 1)", 80 | "status-success-background": "rgba(223, 246, 221, 1)", 81 | 82 | "status-warning-foreground": "rgba(250, 157, 45, 1)", 83 | "status-warning-background": "rgba(255, 244, 206, 1)", 84 | 85 | "background-color": "rgba(255, 255, 255, 1)", 86 | 87 | "text-primary-color": "rgba(0, 0, 0, .9)", 88 | "text-secondary-color": "rgba(0, 0, 0, .55)", 89 | "text-disabled-color": "rgba(0, 0, 0, .38)", 90 | "text-on-communication-background": "rgba(255, 255, 255, 1)", 91 | 92 | "border-subtle-color": "rgba(0, 0, 0, .08)", 93 | 94 | "callout-background-color": "rgba(255, 255, 255, 1)", 95 | "callout-shadow-color": "rgba(0, 0, 0, .132)", 96 | "callout-shadow-secondary-color": "rgba(0, 0, 0, .108)", 97 | 98 | "panel-shadow-color": "rgba(0, 0, 0, .22)", 99 | "panel-shadow-secondary-color": "rgba(0, 0, 0, .18)", 100 | 101 | "focus-pulse-max-color": "rgba(0, 120, 212, 0.35)", 102 | "focus-pulse-min-color": "rgba(0, 120, 212, 0.15)", 103 | 104 | "third-party-icon-filter": "none", 105 | 106 | "icon-folder-color": "#dcb67a", 107 | 108 | "component-menu-selected-item-background": "rgba(244, 244, 244, 1)", 109 | 110 | "component-errorBoundary-border-color": "rgba(218, 10, 0, 1)", 111 | "component-errorBoundary-background-color": "rgba(249, 235, 235, 1)", 112 | 113 | "component-grid-row-hover-color": "rgba(0, 0, 0, .02)", 114 | "component-grid-selected-row-color": "rgba(239, 246, 252, 1)", 115 | "component-grid-focus-border-color": "rgba(0, 120, 212, 1)", 116 | "component-grid-link-selected-row-color": "rgba(16, 110, 190, 1)", 117 | "component-grid-link-hover-color": "rgba(0, 90, 158, 1)", 118 | "component-grid-action-hover-color": "rgba(234, 234, 234, 1)", 119 | "component-grid-action-selected-cell-hover-color": "rgba(222, 236, 249, 1)", 120 | "component-grid-cell-bottom-border-color": "rgba(234, 234, 234, 1)", 121 | 122 | "component-label-default-color": "rgba(244, 244, 244, 1)", 123 | "component-label-default-color-hover": "rgba(218, 218, 218, 1)", 124 | 125 | "component-htmlEditor-background-color": "rgba(255, 255, 255, 1)", 126 | "component-htmlEditor-foreground-color": "rgba(0, 0, 0, 0.9)", 127 | 128 | "nav-header-background": "rgba(255, 255, 255, 1)", 129 | "nav-header-item-hover-background": "rgba(0, 0, 0, 0.02)", 130 | "nav-header-active-item-background": "rgba(0, 0, 0, 0.08)", 131 | "nav-header-text-primary-color": "rgba(0, 0, 0, .9)", 132 | "nav-header-text-secondary-color": "rgba(0, 0, 0, .55)", 133 | "nav-header-text-disabled-color": "rgba(0, 0, 0, .38)", 134 | "nav-header-product-color": "rgba(0, 120, 212, 1)", 135 | 136 | "nav-vertical-background-color": "rgba(0, 0, 0, 0.08)", 137 | "nav-vertical-item-hover-background": "rgba(0, 0, 0, 0.08)", 138 | "nav-vertical-active-group-background": "rgba(0, 0, 0, 0.08)", 139 | "nav-vertical-active-item-background": "rgba(0, 0, 0, 0.2)", 140 | "nav-vertical-text-primary-color": "rgba(0, 0, 0, .9)", 141 | "nav-vertical-text-secondary-color": "rgba(0, 0, 0, .55)", 142 | 143 | "diff-color-original": "rgba(172, 0, 0, 0.1)", 144 | "diff-color-modified": "rgba(51, 153, 51, 0.1)", 145 | 146 | "search-match-background": "rgba(255, 255, 0, 0.6)", 147 | "search-selected-match-background": "rgba(245, 139, 31, 0.8)" 148 | } 149 | -------------------------------------------------------------------------------- /buildpackage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is solely a build script, intended to prep the twin packages for publishing. 3 | */ 4 | 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | const rimraf = require("rimraf"); 8 | const copydir = require("copy-dir"); 9 | const execSync = require("child_process").execSync; 10 | 11 | // AzDevOps Package Constants 12 | const azDevOpsPackageName = "@azurepipelines/azdevops-kube-summary"; 13 | const azDevOpsWebPackConfigFileName = "azdevops.webpack.config.js"; 14 | const azDevOpsPackageFolderName = "azDevOpsPackage"; 15 | 16 | // WebApp Package Constants 17 | const webAppPackageName = "@azurepipelines/webapp-kube-summary"; 18 | const webAppWebPackConfigFileName = "webapp.webpack.config.js"; 19 | const webAppPackageFolderName = "webAppPackage"; 20 | 21 | // The first argument is the full path of the node command. The second element is the full path of the file being executed. 22 | // All the additional arguments are present from the third position going forward. 23 | 24 | var args = process.argv.slice(2); 25 | args.forEach((value, index) => { 26 | console.log(`${index}: ${value}`) 27 | }); 28 | 29 | if (args.length === 0 || args.some(arg => arg.toLowerCase() === azDevOpsPackageFolderName.toLowerCase())) { 30 | prepPackage(azDevOpsPackageFolderName, azDevOpsWebPackConfigFileName, editAzDevOpsPackageJson, azDevOpsDistFilter); 31 | } 32 | 33 | if (args.length === 0 || args.some(arg => arg.toLowerCase() === webAppPackageFolderName.toLowerCase())) { 34 | prepPackage(webAppPackageFolderName, webAppWebPackConfigFileName, editWebAppPackageJson); 35 | } 36 | 37 | function prepPackage(packageFolderName, webpackConfigFileName, editPackageJsonCallback, distFilter) { 38 | const packageFolderPath = createPackageFolder(packageFolderName); 39 | 40 | filesToCopy = ["package.json", "README.md", "LICENSE.txt"]; 41 | console.log(`Copying ${filesToCopy.join(", ")} to ${packageFolderName}`); 42 | copyFiles(filesToCopy, packageFolderPath); 43 | 44 | console.log(`Altering package.json inside ${packageFolderName}`); 45 | const targetPackageJsonPath = path.join(packageFolderPath, "package.json"); 46 | editPackageJson(targetPackageJsonPath, editPackageJsonCallback); 47 | 48 | console.log(`Copying dist folder to ${packageFolderName}`); 49 | copyDistFolder(packageFolderPath, distFilter) 50 | 51 | console.log(`Running webpack for ${packageFolderName}`); 52 | const webPackConfigFilePath = path.join(__dirname, webpackConfigFileName); 53 | execSync(`webpack --mode production --config ${webPackConfigFilePath}`, { 54 | stdio: "inherit" 55 | }); 56 | 57 | console.log(`Packaging ${packageFolderName}`); 58 | execSync("npm pack", { 59 | cwd: packageFolderPath, 60 | stdio: "inherit" 61 | }); 62 | } 63 | 64 | function createPackageFolder(packageFolderName) { 65 | const binFolderPath = path.join(__dirname, "_bin"); 66 | ensureDirExists(binFolderPath); 67 | 68 | const packageFolderPath = path.join(binFolderPath, packageFolderName); 69 | console.log(`Deleting folder: ${packageFolderName}`); 70 | rimraf.sync(packageFolderPath); 71 | 72 | console.log(`Creating folder: ${packageFolderName}`); 73 | fs.mkdirSync(packageFolderPath); 74 | 75 | return packageFolderPath; 76 | } 77 | 78 | function ensureDirExists(dirPath) { 79 | if (!fs.existsSync(dirPath)){ 80 | fs.mkdirSync(dirPath); 81 | } 82 | } 83 | 84 | function copyFiles(filesToCopy, packageFolderPath) { 85 | filesToCopy.forEach(fileName => { 86 | srcFilePath = path.join(__dirname, fileName); 87 | targetFilePath = path.join(packageFolderPath, fileName); 88 | fs.copyFileSync(srcFilePath, targetFilePath); 89 | }); 90 | } 91 | 92 | function copyDistFolder(packageFolderPath, distFilter) { 93 | const srcDistFolderPath = path.join(__dirname, "dist"); 94 | const targetDistFolderPath = path.join(packageFolderPath, "dist"); 95 | copydir.sync(srcDistFolderPath, targetDistFolderPath, distFilter); 96 | } 97 | 98 | function azDevOpsDistFilter(stat, filepath, filename) { 99 | if (stat === 'file' && filename.startsWith("ContentReader")) { 100 | console.log(`Skipping copying ${filename}`); 101 | return false; 102 | } 103 | else if (stat === 'directory' && filename === 'img') { // do not copy 'img' folder containing svg 104 | console.log(`Skipping copying ${filename}`); 105 | return false; 106 | } 107 | 108 | return true; 109 | } 110 | 111 | function editPackageJson(targetPackageJsonPath, editContentCallback) { 112 | let targetPackageJsonContent = fs.readFileSync(targetPackageJsonPath); 113 | let parsedTargetPackageJson = JSON.parse(targetPackageJsonContent); 114 | parsedTargetPackageJson = editContentCallback(parsedTargetPackageJson); 115 | fs.writeFileSync(targetPackageJsonPath, JSON.stringify(parsedTargetPackageJson, null, 2)); 116 | } 117 | 118 | function editAzDevOpsPackageJson(parsedPackageJson) { 119 | parsedPackageJson.name = azDevOpsPackageName; 120 | delete parsedPackageJson.dependencies["azure-devops-ui"]; 121 | delete parsedPackageJson.dependencies["monaco-editor"]; 122 | return parsedPackageJson; 123 | } 124 | 125 | function editWebAppPackageJson(parsedPackageJson) { 126 | parsedPackageJson.name = webAppPackageName; 127 | return parsedPackageJson; 128 | } -------------------------------------------------------------------------------- /docs/ServicesPivot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/azpipelines-kubernetesUI/71c460b4ba2bfda851245a9c8cc562f00a3531ba/docs/ServicesPivot.png -------------------------------------------------------------------------------- /docs/WorkloadsPivot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/azpipelines-kubernetesUI/71c460b4ba2bfda851245a9c8cc562f00a3531ba/docs/WorkloadsPivot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@azurepipelines/azdevops-kube-summary", 3 | "version": "3.6.0", 4 | "description": "Azure DevOps Kubernetes Summary", 5 | "engines": { 6 | "node": ">=8.5.0", 7 | "npm": ">=5.3.0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Microsoft/azpipelines-kubernetesUI.git" 12 | }, 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf ./_bundles && rimraf ./_bin", 15 | "compile": "npm run clean && npm run compilesrc && npm run copy-all", 16 | "build": "npm run compile && node buildpackage.js webAppPackage", 17 | "build-azdevops": "npm run compile && node buildpackage.js azDevOpsPackage", 18 | "build-all": "npm run compile && node buildpackage.js azDevOpsPackage webAppPackage", 19 | "copy-all": "npm run copy-scss && npm run copy-resources && npm run copy-images", 20 | "copy-scss": "copyfiles -u 1 src/**/*.scss ./dist", 21 | "copy-resources": "copyfiles -u 1 src/**/Resources.js ./dist", 22 | "copy-images": "copyfiles -u 1 src/**/*.svg ./dist", 23 | "compilesrc": "tsc -p ./src", 24 | "test": "npm run compiletest && npm run jesttests", 25 | "compiletest": "webpack --mode development --config ./tests/webpack.config.js", 26 | "jesttests": "jest ./dist_tests --verbose --reporters=default --reporters=jest-junit", 27 | "cleantest": "rimraf ./dist_tests", 28 | "ctest": "npm run cleantest && npm run test" 29 | }, 30 | "keywords": [ 31 | "Kubernetes-UI" 32 | ], 33 | "license": "MIT", 34 | "main": "dist/WebUI/Common/KubeSummary.js", 35 | "files": [ 36 | "dist", 37 | "_bundles" 38 | ], 39 | "dependencies": { 40 | "azure-devops-ui": "^1.155.0", 41 | "history": "^4.9.0", 42 | "js-yaml": "3.13.1", 43 | "monaco-editor": "0.16.2", 44 | "react": "16.8.2", 45 | "react-dom": "16.8.2", 46 | "simple-query-string": "^1.3.2" 47 | }, 48 | "devDependencies": { 49 | "@kubernetes/client-node": "^0.8.2", 50 | "@types/enzyme": "^3.9.2", 51 | "@types/enzyme-adapter-react-16": "^1.0.5", 52 | "@types/enzyme-to-json": "^1.5.3", 53 | "@types/history": "^4.7.2", 54 | "@types/jest": "^23.3.14", 55 | "@types/react": "16.8.2", 56 | "@types/react-dom": "16.8.2", 57 | "base64-inline-loader": "^1.1.1", 58 | "circular-dependency-plugin": "^5.0.2", 59 | "copy-dir": "^0.4.0", 60 | "copy-webpack-plugin": "^4.6.0", 61 | "copyfiles": "^2.1.0", 62 | "css-loader": "^1.0.1", 63 | "enzyme": "^3.9.0", 64 | "enzyme-adapter-react-16": "^1.13.1", 65 | "enzyme-to-json": "^3.3.5", 66 | "file-loader": "~2.0.0", 67 | "glob": "^7.1.4", 68 | "jest": "^24.8.0", 69 | "jest-junit": "^6.4.0", 70 | "node-sass": "^4.11.0", 71 | "recursive-copy": "^2.0.10", 72 | "rimraf": "^2.6.3", 73 | "sass-loader": "~7.1.0", 74 | "style-loader": "~0.23.1", 75 | "terser-webpack-plugin": "^1.2.4", 76 | "ts-loader": "~5.2.2", 77 | "typescript": "^2.9.2", 78 | "uglifyjs-webpack-plugin": "^2.1.3", 79 | "webpack": "^4.31.0", 80 | "webpack-cli": "^3.3.2" 81 | }, 82 | "bugs": { 83 | "url": "https://github.com/Microsoft/azpipelines-kubernetesUI/issues" 84 | }, 85 | "homepage": "https://github.com/Microsoft/azpipelines-kubernetesUI#readme", 86 | "author": "Azure Pipelines", 87 | "jest-junit": { 88 | "suiteName": "L0 Uts", 89 | "outputDirectory": ".", 90 | "outputName": "./dist_tests/jest-l0-uts.xml" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /scripts/FailIfDirty.cmd: -------------------------------------------------------------------------------- 1 | @if not defined _echo echo off 2 | SETLOCAL 3 | 4 | REM A user who installs Git in a non-standard location can set _GIT_PATH 5 | REM in the global environment so this script can find it. 6 | IF "%_GIT_PATH%" == "" SET "_GIT_PATH=%ProgramW6432%\Git\cmd\git.exe" 7 | IF NOT EXIST "%_GIT_PATH%" SET "_GIT_PATH=%ProgramFiles(x86)%\Git\cmd\git.exe" 8 | IF NOT EXIST "%_GIT_PATH%" SET "_GIT_PATH=%LocalAppData%\Programs\Git\cmd\git.exe" 9 | 10 | REM Normalize the source tree path 11 | PUSHD %~dp0.. 12 | SET _SRC_DIR=%CD% 13 | POPD 14 | 15 | ECHO Checking for any pending changes in %_SRC_DIR%... 16 | SET _ANY_CHANGES=0 17 | PUSHD %_SRC_DIR% 18 | 19 | REM Check for pending changes in the source tree. 20 | IF EXIST "%_GIT_PATH%" ( 21 | @FOR /F "tokens=1,*" %%A IN ('"%_GIT_PATH%" status -s') DO @SET _ANY_CHANGES=1 & ECHO ##vso[task.logissue type=error;sourcepath=%%B;linenumber=1;columnnumber=1;code=100;]Unexpected pending change [%%A] after build completion 22 | ) ELSE ( 23 | ECHO WARNING: Git for Windows not installed; unable to check the source tree. 1>&2 24 | ) 25 | 26 | @IF %_ANY_CHANGES% EQU 1 ECHO ##vso[task.logissue type=error;sourcepath=Scripts/failifdirty.cmd;linenumber=25;columnnumber=1;code=100;]One or more pending changes were found after the build. There were none when it started. This usually indicates that generated files were not submitted. 27 | 28 | POPD 29 | EXIT /B %_ANY_CHANGES% 30 | -------------------------------------------------------------------------------- /src/Contracts/Contracts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import * as K8sTypes from "@kubernetes/client-node"; 7 | import { IImageDetails } from "./Types"; 8 | 9 | export interface IKubeService { 10 | getPods(labelSelector?: string): Promise; 11 | 12 | getDeployments(): Promise; 13 | 14 | getServices(): Promise; 15 | 16 | getReplicaSets(): Promise; 17 | 18 | getDaemonSets(): Promise; 19 | 20 | getStatefulSets(): Promise; 21 | 22 | getPodLog(podName: string, podContainerName?: string): Promise; 23 | } 24 | 25 | export enum KubeImage { 26 | zeroData = "zeroData", 27 | zeroResults = "zeroResults", 28 | zeroWorkloads = "zeroWorkloads", 29 | resourceDeleted = "resourceDeleted", 30 | resourceAccessDenied ="resourceAccessDenied" 31 | } 32 | 33 | export interface IImageService { 34 | hasImageDetails(listImages: Array): Promise; 35 | 36 | getImageDetails(imageName: string): Promise; 37 | 38 | getImageProvenances(imageNames: string[]): Promise; 39 | } 40 | 41 | export interface ITelemetryService { 42 | markTimeToInteractive(scenarioName: string, additionalProperties?: { [key: string]: any }): void; 43 | onClickTelemetry(source: string, additionalProperties?: { [key: string]: any }): void; 44 | scenarioStart(scenarioName: string, additionalProperties?: { [key: string]: any }): void; 45 | scenarioEnd(scenarioName: string, additionalProperties?: { [key: string]: any }): void; 46 | } 47 | 48 | /** 49 | * https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase 50 | **/ 51 | export enum PodPhase { 52 | Pending = "Pending", 53 | Running = "Running", 54 | Succeeded = "Succeeded", 55 | Failed = "Failed", 56 | Unknown = "Unknown", 57 | Completed = "Completed", 58 | CrashLoopBackOff = "CrashLoopBackOff", 59 | } 60 | 61 | export enum ResourceErrorType { 62 | Deleted = "Deleted", 63 | AccessDenied = "AccessDenied", 64 | None = "None", 65 | NotInitialized = "NotInitialized" 66 | } -------------------------------------------------------------------------------- /src/Contracts/KubeServiceBase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import * as K8sTypes from "@kubernetes/client-node"; 7 | import { IKubeService } from "./Contracts"; 8 | 9 | export enum KubeResourceType { 10 | Pods = 1, 11 | Deployments = 2, 12 | Services = 4, 13 | ReplicaSets = 8, 14 | DaemonSets = 16, 15 | StatefulSets = 32, 16 | } 17 | 18 | export abstract class KubeServiceBase implements IKubeService { 19 | getPods(labelSelector?: string): Promise { 20 | return this.fetch(KubeResourceType.Pods, labelSelector); 21 | } 22 | 23 | getDeployments(): Promise { 24 | return this.fetch(KubeResourceType.Deployments); 25 | } 26 | 27 | getServices(): Promise { 28 | return this.fetch(KubeResourceType.Services); 29 | } 30 | 31 | getReplicaSets(): Promise { 32 | return this.fetch(KubeResourceType.ReplicaSets); 33 | } 34 | 35 | getDaemonSets(): Promise { 36 | return this.fetch(KubeResourceType.DaemonSets); 37 | } 38 | 39 | getStatefulSets(): Promise { 40 | return this.fetch(KubeResourceType.StatefulSets); 41 | } 42 | 43 | abstract getPodLog(podName: string, podContainerName?: string): Promise; 44 | 45 | abstract fetch(resourceType: KubeResourceType, labelSelector?: string): Promise; 46 | } -------------------------------------------------------------------------------- /src/Contracts/Types.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | export interface IImageDetails { 7 | imageName: string; 8 | imageUri: string; 9 | baseImageName: string; 10 | distance?: number; 11 | imageType: string; 12 | mediaType: string; 13 | tags: Array; 14 | layerInfo: Array; 15 | runId: number; 16 | pipelineVersion: string; 17 | pipelineName: string; 18 | pipelineId: string; 19 | jobName: string; 20 | imageSize: string; 21 | createTime?: Date; 22 | } 23 | 24 | export interface IImageLayer { 25 | directive: string; 26 | arguments: string; 27 | size: string; 28 | createdOn: Date; 29 | } -------------------------------------------------------------------------------- /src/Resources.ts: -------------------------------------------------------------------------------- 1 | export const AgeText = "Created"; 2 | export const NameText = "Name"; 3 | export const StatusText = "Status"; 4 | export const ConditionsText = "Conditions"; 5 | export const ImageText = "Image"; 6 | export const PodsDetailsText = "Pod"; 7 | export const StrategyText = "Strategy"; 8 | export const ReplicasCountText = "Pods #"; 9 | export const DeploymentsDetailsText = "Deployments"; 10 | export const ClusterIPText = "Cluster IP"; 11 | export const ExternalIPText = "External IP"; 12 | export const TypeText = "Type"; 13 | export const ReplicaSetText = "ReplicaSet"; 14 | export const PackageText = "Package"; 15 | export const PipelineText = "Pipeline"; 16 | export const PodsText = "Pods"; 17 | export const PodText = "Pod"; 18 | export const PortText = "Port"; 19 | export const NamespaceLabelText = "Namespace:"; 20 | export const NamespaceWithValueText = "Namespace: {0}"; 21 | export const PivotServiceText = "Services"; 22 | export const PivotWorkloadsText = "Workloads"; 23 | export const PodIP = "IP"; 24 | export const Created = "Created"; 25 | export const Deployment = "Deployment: {0}"; 26 | export const ReplicaSet = "ReplicaSet: {0}"; 27 | export const ServiceCreatedWithPipelineText = "Created {0} by {1}"; 28 | export const RunInformationForWorkload = "{0} on {1}"; 29 | export const OverviewText = "Overview"; 30 | export const LabelsText = "Labels"; 31 | export const SelectorText = "Selector"; 32 | export const SessionAffinityText = "Session affinity"; 33 | export const AssociatedPodsText = "Associated pods"; 34 | export const DaemonSetText = "DaemonSet"; 35 | export const StatefulSetText = "StatefulSet"; 36 | export const DeploymentText = "Deployment"; 37 | export const LearnMoreText = "Learn more"; 38 | export const NoPodsForSvcText = "This service currently does not map to any pods"; 39 | export const SucceededText = "Succeeded"; 40 | export const InProgressText = "InProgress"; 41 | export const FindByNameText = "Filter by {0} name"; 42 | export const KindText = "Kind"; 43 | export const NoItemsText = "No Items found"; 44 | export const PodDetailsHeader = "Pod details"; 45 | export const JobText = "Job"; 46 | export const AnnotationsText = "Annotations"; 47 | export const RestartPolicyText = "Restart policy"; 48 | export const QoSClassText = "QoS class"; 49 | export const NodeText = "Node"; 50 | export const PodsListHeaderText = "Pods"; 51 | export const LogsText = "Logs"; 52 | export const YamlText = "YAML"; 53 | export const NoPodsFoundText = "No pods are detected in this Kubernetes workload"; 54 | export const LoadingText = "Loading..."; 55 | export const OtherWorkloadsText = "Other workloads"; 56 | export const CreatedAgo = "Created {0}"; 57 | export const ServiceDetails = "Service details"; 58 | export const WorkloadText = "Workload"; 59 | export const WorkloadDetails = "{0} details"; 60 | export const PodStatusPending = "Pending"; 61 | export const MoreImagesText = "{0} and {1} more"; 62 | export const LearnMoreKubeResourceText = "Learn more about Kubernetes resource"; 63 | export const DeployKubeResourceText = "Use this resource to deploy workloads and services"; 64 | export const StartUsingKubeResourceText = "Start using resource!"; 65 | export const KubernetesAuthValidationTitleText = "Validate to connect"; 66 | export const KubernetesAuthValidationHelpText = "The service account mapped to this resource doesn't have the necessary permissions to read workloads and services."; 67 | export const KubernetesResourceDeletedAltText = "Unable to connect to the cluster and namespace"; 68 | export const KubernetesResourceDeletedHelpText = "Unable to connect to the cluster and namespace chosen for this resource. "; 69 | export const StartingUsingServiceText = "You can also deploy services and track them in here"; 70 | export const DeployServices = "Deploy services!"; 71 | export const DeployWorkloads = "Deploy workloads!"; 72 | export const WorkloadsZeroDataText = "You can also deploy all kinds of workloads like deployment, replicaset, daemonset etc., and track them in here"; 73 | export const NoPodsForSvcLinkText = "Learn more about mapping a service"; 74 | export const NoPodsText = "No pods"; 75 | export const ServiceText = "service"; 76 | export const NoResultsFoundText = "No results found"; 77 | export const DigestText = "Digest"; 78 | export const TarIdText = "Tar ID"; 79 | export const ImageTypeText = "Image type"; 80 | export const MediaTypeText = "Media type"; 81 | export const RegistryText = "Registry"; 82 | export const ImageSizeText = "Image size"; 83 | export const ImageDetailsHeaderText = "Image details"; 84 | export const AllPodsRunningText = "All pods are running"; 85 | export const PodsNotReadyText = "{0} pods are not ready"; 86 | export const PodNotReadyText = "1 pod is not ready"; 87 | export const LayersText = "Layers"; 88 | export const CommandText = "Command"; 89 | export const SizeText = "Size"; 90 | export const SummaryHeaderSubTextFormat = "{0} (cluster)"; 91 | export const TagsText = "Tags"; 92 | export const LoadingPodsSpinnerLabel = "Loading associated pods..."; 93 | export const LoadingServicesSpinnerLabel = "Loading services..."; 94 | export const CopyExternalIp = "Copy External IP to clipboard"; 95 | export const CopiedExternalIp = "Copied!"; 96 | export const ImageDetailsUnavailableText = "Image details available only for images built in Azure Pipelines"; 97 | export const PodDetailsSubheader = "Pods in {0}"; 98 | export const ClusterLinkHelpText = "Navigation to cluster details is only available for the Azure provider"; 99 | export const ImageDetailsExportText = "Export image details"; 100 | export const ImageDetailsExportTooltip = "Download details associated with this image to a local file." 101 | -------------------------------------------------------------------------------- /src/WebUI/Common/ContentReader.scss: -------------------------------------------------------------------------------- 1 | @import "azure-devops-ui/_coreStyles.scss"; 2 | 3 | .k8s-monaco-reader-default { 4 | /* Override the default Monaco themed line number colors to meet A11y 4.5:1 contrast requirements. 5 | 'vs' change from teal #4191AD (3.6:1) to light gray #737373 (4.7:1) (Same as Code hub). 6 | 'vs-dark' change from dark gray #5A5A5A (2.4:1) to light gray #A19F9D (6.3:1). */ 7 | .monaco-editor.vs .line-numbers { 8 | color: $secondary-text; 9 | } 10 | 11 | .monaco-editor.vs-dark .line-numbers { 12 | color: $neutral-60; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WebUI/Common/ContentReader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { CardContent } from "azure-devops-ui/Components/Card/CardContent"; 7 | import { CustomCard } from "azure-devops-ui/Components/Card/CustomCard"; 8 | import { css } from "azure-devops-ui/Util"; 9 | import * as React from "react"; 10 | 11 | import "./ContentReader.scss"; 12 | 13 | // basic editor functionality 14 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 15 | // languages supported in editor 16 | import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution"; 17 | // additional functionality of editor 18 | import "monaco-editor/esm/vs/editor/browser/controller/coreCommands"; 19 | import "monaco-editor/esm/vs/editor/contrib/find/findController"; 20 | import "monaco-editor/esm/vs/editor/contrib/message/messageController"; 21 | 22 | export interface IReaderProps { 23 | text: string; 24 | options?: monaco.editor.IEditorConstructionOptions; 25 | contentClassName?: string; 26 | className?: string; 27 | } 28 | 29 | export class ContentReader extends React.Component { 30 | public render(): JSX.Element { 31 | return ( 32 | // monaco-editor class added here to have the same theme as monaco. 33 | 34 | 35 |
36 |
41 |
42 | 43 | 44 | ); 45 | } 46 | 47 | public componentDidMount(): void { 48 | window.addEventListener("resize", this._onResizeHandler); 49 | } 50 | 51 | public componentWillUnmount(): void { 52 | window.removeEventListener("resize", this._onResizeHandler); 53 | this._disposeEditor(); 54 | } 55 | 56 | private _onResizeHandler = () => { 57 | if (this._editor) { 58 | this._editor.layout(); 59 | } 60 | } 61 | 62 | private _createEditor = (innerRef) => { 63 | if (innerRef) { 64 | const text = this.props.text || ""; 65 | this._disposeEditor(); 66 | 67 | this._editor = monaco.editor.create(innerRef, { 68 | readOnly: true, 69 | renderWhitespace: "all", 70 | fontSize: 13, 71 | lineHeight: 20, 72 | minimap: { enabled: false }, 73 | scrollbar: { horizontalScrollbarSize: 16 }, 74 | lineNumbers: "on", 75 | extraEditorClassName: "k8s-monaco-editor", 76 | theme: "vs", 77 | language: "yaml", 78 | ...this.props.options, 79 | value: text 80 | }); 81 | } 82 | } 83 | 84 | private _disposeEditor(): void { 85 | if (this._editor) { 86 | try { 87 | this._editor.dispose(); 88 | } 89 | catch (e) { } 90 | 91 | this._editor = undefined; 92 | } 93 | } 94 | 95 | private _editor: monaco.editor.IStandaloneCodeEditor | undefined; 96 | } -------------------------------------------------------------------------------- /src/WebUI/Common/DefaultImageLocation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { KubeImage } from "../../Contracts/Contracts"; 7 | 8 | export class DefaultImageLocation { 9 | public static getImageLocation = (image: KubeImage): string | undefined => { 10 | return DefaultImageLocation._imageLocations.get(image) || DefaultImageLocation._imageLocations.get(KubeImage.zeroData); 11 | } 12 | 13 | private static _imageLocations: Map = new Map([ 14 | [KubeImage.zeroData, require("../../img/zero-data.svg")], 15 | [KubeImage.zeroResults, require("../../img/zero-results.svg")], 16 | [KubeImage.zeroWorkloads, require("../../img/zero-workloads.svg")], 17 | [KubeImage.resourceDeleted, require("../../img/zero-data.svg")], 18 | [KubeImage.resourceAccessDenied, require("../../img/zero-data.svg")], 19 | ]); 20 | } -------------------------------------------------------------------------------- /src/WebUI/Common/KubeCardWithTable.scss: -------------------------------------------------------------------------------- 1 | @import "azure-devops-ui/_coreStyles.scss"; 2 | 3 | .k8s-pods-status-count { 4 | padding-left: $spacing-8; 5 | } 6 | 7 | .external-ip-cell { 8 | .external-ip-cell-text { 9 | padding-right: $spacing-4; 10 | } 11 | .external-ip-copy-icon.kube-text-copy.subtle { 12 | padding: 2px; 13 | vertical-align: bottom; 14 | cursor: pointer; 15 | } 16 | } -------------------------------------------------------------------------------- /src/WebUI/Common/KubeConsumer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | class KubeConsumer { 7 | public static setReaderComponent(reader?: (props?: any) => React.ReactNode): void { 8 | if (reader) { 9 | KubeConsumer._readerComponentFunc = reader; 10 | } 11 | } 12 | 13 | public static getReaderComponent(props?: any): React.ReactNode { 14 | if (KubeConsumer._readerComponentFunc) { 15 | return KubeConsumer._readerComponentFunc(props); 16 | } 17 | 18 | return null; 19 | } 20 | 21 | private static _readerComponentFunc?: (props?: any) => React.ReactNode; 22 | } 23 | 24 | export function getContentReaderComponent(props?: any): React.ReactNode { 25 | return KubeConsumer.getReaderComponent(props); 26 | } 27 | 28 | export function setContentReaderComponent(reader?: (props?: any) => React.ReactNode): void { 29 | KubeConsumer.setReaderComponent(reader); 30 | } -------------------------------------------------------------------------------- /src/WebUI/Common/KubeFilterBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ConditionalChildren } from "azure-devops-ui/ConditionalChildren"; 7 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 8 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 9 | import { DropdownFilterBarItem } from "azure-devops-ui/Dropdown"; 10 | import { FilterBar } from "azure-devops-ui/FilterBar"; 11 | import { IListSelection } from "azure-devops-ui/List"; 12 | import { IListBoxItem } from "azure-devops-ui/ListBox"; 13 | import { KeywordFilterBarItem } from "azure-devops-ui/TextFilterBarItem"; 14 | import { Filter } from "azure-devops-ui/Utilities/Filter"; 15 | import * as React from "react"; 16 | import * as Resources from "../../Resources"; 17 | import { IVssComponentProperties } from "../Types"; 18 | 19 | /* Including from office-ui-fabric-react to avoid direct dependency on office-ui-fabric-react */ 20 | enum SelectionMode { 21 | none = 0, 22 | single = 1, 23 | multiple = 2, 24 | } 25 | 26 | export const NameKey: string = "nameKey"; 27 | export const TypeKey: string = "typeKey"; 28 | 29 | export interface IFilterComponentProperties extends IVssComponentProperties { 30 | filter: Filter; 31 | keywordPlaceHolder: string; 32 | pickListPlaceHolder: string; 33 | filterToggled: ObservableValue; 34 | selection: IListSelection; 35 | listItems: IListBoxItem<{}>[]; 36 | addBottomPadding?: boolean; 37 | } 38 | 39 | export class KubeFilterBar extends React.Component { 40 | 41 | public render(): React.ReactNode { 42 | return ( 43 | 44 | 45 | 46 | 54 | 55 | {this.props.addBottomPadding &&
} 56 | 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/WebUI/Common/KubeSummary.scss: -------------------------------------------------------------------------------- 1 | .kubernetes-container { 2 | 3 | // cards have margins 4 | .k8s-card-padding + .k8s-card-padding { 5 | margin-top: 16px; 6 | } 7 | 8 | /*Css to adjust the alignment of text between picklist and text search. Revisit when we move from picklist to dropdown (fabric-free)*/ 9 | .keyword-search.bolt-text-field { 10 | padding-bottom: 8px; 11 | } 12 | 13 | .loading-pods { 14 | height: 200px; 15 | } 16 | 17 | .details-card-content { 18 | flex-wrap: wrap; 19 | width: 100%; 20 | 21 | .details-card-row-size { 22 | &:not(.first-row) { 23 | padding-top: 6px; 24 | } 25 | 26 | padding-bottom: 6px; 27 | position: relative; 28 | 29 | &.pod-image-data { 30 | padding-top: 0px; 31 | padding-bottom: 0px; 32 | } 33 | } 34 | 35 | .details-card-info-field-size { 36 | width: 96px; 37 | min-width: 96px; 38 | 39 | &.pod-image-key { 40 | padding-top: 6px; 41 | padding-bottom: 6px; 42 | } 43 | } 44 | 45 | .details-card-value-field-size { 46 | position: absolute; 47 | width: calc(100% - 136px); // 136px = label width 96px + left padding of card 20px + gap required between label and value 20px 48 | margin-left: 136px; 49 | 50 | &.pod-image-link { 51 | padding: 6px; 52 | margin-left: 124px; 53 | } 54 | 55 | &.pod-image-nolink { 56 | padding: 6px; 57 | margin-left: 130px; 58 | } 59 | } 60 | } 61 | 62 | .k8s-user-action-margin>:last-child { 63 | margin-bottom: 4px; 64 | } 65 | } -------------------------------------------------------------------------------- /src/WebUI/Common/KubeZeroData.scss: -------------------------------------------------------------------------------- 1 | .k8s-zero-data-max-width { 2 | width: 100%; 3 | } 4 | 5 | // deployments/services are zero 6 | .k8s-zero-data { 7 | margin-bottom: 100px; 8 | } 9 | 10 | // only deployment are zero 11 | .k8s-zero-workloads-data { 12 | margin-bottom: 80px; 13 | margin-top: 80px; 14 | } 15 | 16 | // only services are zero 17 | .k8s-zero-services-data, 18 | // zero associated pods 19 | .k8s-zero-service-pods-data { 20 | margin-bottom: 80px; 21 | } 22 | 23 | // filter results are zero 24 | .k8s-zero-filter-data { 25 | margin: 106px 0px; 26 | } -------------------------------------------------------------------------------- /src/WebUI/Common/PageTopHeader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { CustomHeader, HeaderIcon, HeaderTitle, HeaderTitleArea, HeaderTitleRow, TitleSize } from "azure-devops-ui/Header"; 7 | import { IStatusProps, Status, StatusSize } from "azure-devops-ui/Status"; 8 | import { Tooltip } from "azure-devops-ui/TooltipEx"; 9 | import * as React from "react"; 10 | import { IVssComponentProperties } from "../Types"; 11 | 12 | export interface IPageTopHeader extends IVssComponentProperties { 13 | title: string; 14 | statusProps?: IStatusProps; 15 | statusTooltip?: string; 16 | } 17 | 18 | export class PageTopHeader extends React.Component { 19 | public render(): React.ReactNode { 20 | const { title } = this.props; 21 | return ( 22 | 23 | {this._getHeaderIcon()} 24 | 25 | 26 | {title || ""} 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | private _getHeaderIcon(): JSX.Element | undefined { 34 | const statusComponent = (sProps, cName) => ; 35 | let { statusTooltip, statusProps } = this.props; 36 | statusTooltip = statusTooltip || ""; 37 | statusProps = statusProps ? { ...statusProps, ariaLabel: statusTooltip } : statusProps; 38 | 39 | return statusProps && 40 | 44 | 45 |
{statusComponent(statusProps, className)}
46 |
47 | }} 48 | />; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/WebUI/Common/ResourceStatus.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { IStatusProps, Status, StatusSize } from "azure-devops-ui/Status"; 7 | import { Tooltip } from "azure-devops-ui/TooltipEx"; 8 | import { css } from "azure-devops-ui/Util"; 9 | import * as React from "react"; 10 | import { IVssComponentProperties } from "../Types"; 11 | 12 | export interface IResourceStatusProps extends IVssComponentProperties { 13 | statusProps: IStatusProps | undefined; 14 | statusSize?: StatusSize; 15 | statusDescription?: string; 16 | customDescription?: React.ReactNode; 17 | toolTipText?: string; 18 | } 19 | 20 | export class ResourceStatus extends React.Component { 21 | 22 | public render(): React.ReactNode { 23 | return ( 24 |
25 | { 26 | this.props.toolTipText ? 27 | 28 | {this._getStatus()} 29 | : this._getStatus() 30 | } 31 |
32 | ); 33 | } 34 | 35 | private _getStatus(): JSX.Element { 36 | return ( 37 | 38 | {this.props.statusProps && 39 | } 40 |
41 | { 42 | this.props.statusDescription && 43 | {this.props.statusDescription} 44 | } 45 | { 46 | this.props.customDescription 47 | } 48 |
49 |
50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /src/WebUI/Common/Tags.scss: -------------------------------------------------------------------------------- 1 | .k8s-tags .k8s-tag-pill:not(:last-child) { 2 | margin-right: 8px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Common/Tags.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { Pill, PillSize } from "azure-devops-ui/Pill"; 7 | import { PillGroup, PillGroupOverflow } from "azure-devops-ui/PillGroup"; 8 | import { css } from "azure-devops-ui/Util"; 9 | import { IVssComponentProperties } from "../Types"; 10 | import { Utils } from "../Utils"; 11 | import "./Tags.scss"; 12 | import React = require("react"); 13 | 14 | export interface ITagsProperties extends IVssComponentProperties { 15 | items: { [key: string]: string }; 16 | showOnlyValues?: boolean; 17 | } 18 | 19 | export class Tags extends React.Component { 20 | darkColor: any; 21 | 22 | public render(): React.ReactNode { 23 | const items = this.props.showOnlyValues ? this.props.items : Utils.getPillTags(this.props.items); 24 | return ( 25 | 26 | { 27 | items && (items as string[]).map((tagText, index) => {tagText}) 28 | } 29 | 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/WebUI/Constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | export const enum PodsRightPanelTabsKeys { 7 | PodsDetailsKey = "pod-details", 8 | PodsLogsKey = "pod-logs", 9 | PodsYamlKey = "pod-yaml" 10 | } 11 | 12 | export const enum SelectedItemKeys { 13 | ReplicaSetKey = "replica-set", 14 | DaemonSetKey = "daemon-set", 15 | StatefulSetKey = "stateful-set", 16 | OrphanPodKey = "orphan-pod", 17 | ServiceItemKey = "service-item", 18 | ImageDetailsKey = "image-details", 19 | PodDetailsKey = "pod-details" 20 | } 21 | 22 | export namespace WorkloadsEvents { 23 | export const DeploymentsFetchedEvent: string = "DEPLOYMENTS_FETCHED_EVENT"; 24 | export const ReplicaSetsFetchedEvent: string = "REPLICA_SETS_FETCHED_EVENT"; 25 | export const DaemonSetsFetchedEvent: string = "DAEMON_SETS_FETCHED_EVENT"; 26 | export const StatefulSetsFetchedEvent: string = "STATEFUL_SETS_FETCHED_EVENT"; 27 | export const WorkloadPodsFetchedEvent: string = "WORKLOAD_PODS_FETCHED_EVENT"; 28 | export const WorkloadsFoundEvent: string = "NON_ZERO_WORKLOADS_FOUND_EVENT"; 29 | export const ZeroDeploymentsFoundEvent: string = "ZERO_DEPLOYMENTS_FOUND_EVENT"; 30 | } 31 | 32 | export namespace ServicesEvents { 33 | export const ServicesFetchedEvent: string = "SERVICES_FETCHED_EVENT"; 34 | export const ServicePodsFetchedEvent: string = "SERVICE_PODS_FETCHED_EVENT"; 35 | export const ServicesFoundEvent: string = "NON_ZERO_SERVICES_FOUND_EVENT"; 36 | } 37 | 38 | export namespace PodsEvents { 39 | export const PodsFetchedEvent: string = "ALL_PODS_FETCHED_EVENT"; 40 | export const LabelledPodsFetchedEvent: string = "LABELLED_PODS_FETCHED_EVENT"; 41 | } 42 | 43 | export namespace ImageDetailsEvents { 44 | export const HasImageDetailsEvent: string = "HAS_IMAGE_DETAILS_EVENT"; 45 | export const SetImageDetailsEvent: string = "SET_IMAGE_DETAILS_EVENT"; 46 | } 47 | 48 | export namespace HyperLinks { 49 | export const WorkloadsLink: string = "https://go.microsoft.com/fwlink/?linkid=2083857"; 50 | export const LinkToPodsUsingLabelsLink: string = "https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/"; 51 | export const ServicesLink: string = "https://go.microsoft.com/fwlink/?linkid=2083858"; 52 | export const ResourceDeletedLink: string = "https://go.microsoft.com/fwlink/?linkid=2083857"; 53 | export const ResourceAccessDeniedLink: string = "https://go.microsoft.com/fwlink/?linkid=2083857"; 54 | } 55 | 56 | export namespace Scenarios{ 57 | export const Services = "KubeSummary.ServicesList"; 58 | export const PodsList = "KubeSummary.PodsList"; 59 | export const ImageDetails = "KubeSummary.ImageDetails"; 60 | export const PodsDetails = "KubeSummary.PodsDetails"; 61 | export const ServiceDetails = "KubeSummary.ServiceDetails"; 62 | export const PodOverview = "KubeSummary.PodOverview"; 63 | export const PodLogs = "KubeSummary.PodLogs"; 64 | export const PodYaml = "KubeSummary.PodYaml"; 65 | export const WorkloadDetails = "KubeSummary.WorkloadDetails"; 66 | export const Workloads = "KubeSummary.Workloads"; 67 | } -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/Actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { Initializable } from "./Factory"; 7 | 8 | export type ActionListener = (payload: T) => void; 9 | 10 | export interface IEmptyActionPayload { 11 | } 12 | 13 | export interface IActionPayload { 14 | } 15 | 16 | export abstract class ActionCreatorBase extends Initializable { 17 | } 18 | 19 | export abstract class ActionsHubBase extends Initializable { 20 | } 21 | 22 | export class Action { 23 | /** 24 | * A mutex to ensure that only one action is executing at any time. 25 | * This prevents cascading actions. 26 | */ 27 | private static executing: boolean = false; 28 | 29 | private listeners: ActionListener[] = []; 30 | 31 | public invoke(payload: T): void { 32 | if (Action.executing) { 33 | throw new Error("Cannot invoke an action from inside another action."); 34 | } 35 | 36 | Action.executing = true; 37 | 38 | try { 39 | this.listeners.forEach(listener => { 40 | listener(payload); 41 | }); 42 | } finally { 43 | Action.executing = false; 44 | } 45 | } 46 | 47 | /** 48 | * Add listener to the action 49 | * @param listener Listener to add 50 | */ 51 | public addListener(listener: ActionListener): void { 52 | this.listeners.push(listener); 53 | } 54 | 55 | /** 56 | * Remove listener from the action 57 | * @param listener Listener to remove 58 | */ 59 | public removeListener(listener: ActionListener): void { 60 | const index = this.listeners.indexOf(listener); 61 | if (index >= 0) { 62 | this.listeners.splice(index, 1); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/ActionsCreatorManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { Manager, INewable, Initializable } from "./Factory"; 7 | 8 | export class ActionsCreatorManager extends Manager { 9 | 10 | /** 11 | * Get an instance of the action creator. Use action creator in cases where data needs 12 | * to be fetched from source before invoking an action. 13 | */ 14 | public static GetActionCreator(actionCreatorClass: INewable, instanceId?: string): T { 15 | return super.getInstance(ActionsCreatorManager).getObject(actionCreatorClass, instanceId) as T; 16 | } 17 | 18 | public static CreateActionCreator(actionCreatorClass: INewable, instanceId: string, args: U): T { 19 | return super.getInstance(ActionsCreatorManager).createObject(actionCreatorClass, instanceId, args) as T; 20 | } 21 | 22 | public static DeleteActionCreator(actionCreatorClass: INewable, instanceId?: string): void { 23 | super.getInstance(ActionsCreatorManager).removeObject(actionCreatorClass, instanceId); 24 | } 25 | 26 | public static dispose() { 27 | return super.getInstance(ActionsCreatorManager).dispose(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/ActionsHubManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { Manager, INewable, Initializable } from "./Factory"; 7 | 8 | export class ActionsHubManager extends Manager { 9 | 10 | /** 11 | * Get an instance of actions hub. Use actions hub when there is no logic that needs to be performed as part of actions. 12 | */ 13 | public static GetActionsHub(actionsHubClass: INewable, instanceId?: string): T { 14 | return super.getInstance(ActionsHubManager).getObject(actionsHubClass, instanceId) as T; 15 | } 16 | 17 | /** 18 | * Get all instances of the actions hub for the class. This is used by unit tests to raise actions that changes stores deep in the 19 | * hierarchy. 20 | */ 21 | public static GetAllActionsHub(actionsHubClass: INewable): T[] { 22 | return super.getInstance(ActionsHubManager).getAllObjects(actionsHubClass) as T[]; 23 | } 24 | 25 | public static dispose() { 26 | return super.getInstance(ActionsHubManager).dispose(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/Factory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | export interface INewable { 7 | new (args?: U): T; 8 | getKey: () => string; 9 | initialize?: (instanceId?: string) => void; 10 | } 11 | 12 | export abstract class Singleton { 13 | 14 | constructor() { 15 | if (!Singleton._allowPrivateInstantiation) { 16 | throw new Error("Error: Instantiating an object of Singleton class is not allowed. Please use the instance method"); 17 | } 18 | } 19 | 20 | protected static getInstance(className: new () => T): T { 21 | if (!this._instance) { 22 | Singleton._allowPrivateInstantiation = true; 23 | this._instance = new className(); 24 | Singleton._allowPrivateInstantiation = false; 25 | } 26 | return this._instance; 27 | } 28 | 29 | protected static dispose(): void { 30 | this._instance = null; 31 | } 32 | 33 | private static _allowPrivateInstantiation: boolean = false; 34 | private static _instance: Singleton | null = null; 35 | } 36 | 37 | export class Factory { 38 | 39 | public static create(className: INewable, args?: U): T { 40 | return this.createObject(className, args); 41 | } 42 | 43 | public static createObject(className: INewable, args?: U | null): T { 44 | if (args) { 45 | return new className(args); 46 | } else { 47 | let instance = Object.create(className.prototype); 48 | try { 49 | const constructed = instance.constructor(args); 50 | if (constructed) { 51 | return constructed; 52 | } else { 53 | return instance; 54 | } 55 | } catch (e) { 56 | return new instance.constructor(args); 57 | } 58 | } 59 | } 60 | } 61 | 62 | export abstract class KeyMonikerProvider { 63 | public static getKey(): string { 64 | throw new Error("This method needs to be implemented in derived classes"); 65 | } 66 | } 67 | 68 | export abstract class Initializable extends KeyMonikerProvider { 69 | public abstract initialize(instanceId?: string | null): void; 70 | } 71 | 72 | export abstract class BaseManager extends Singleton { 73 | 74 | constructor() { 75 | super(); 76 | this._instanceMap = {}; 77 | } 78 | 79 | protected dispose() { 80 | 81 | Object.keys(this._instanceMap).forEach((key: string) => { 82 | this._deleteInstance(key); 83 | }); 84 | 85 | this._instanceMap = {}; 86 | } 87 | 88 | protected getAllObjects(instanceClass: INewable): T[] { 89 | let instanceKey = instanceClass.getKey().toLowerCase(); 90 | let instances: T[] = []; 91 | for (let instance in this._instanceMap) { 92 | if (this._instanceMap.hasOwnProperty(instance)) { 93 | if (instance.indexOf(instanceKey) === 0) { 94 | instances.push(this._instanceMap[instance]); 95 | } 96 | } 97 | } 98 | 99 | return instances; 100 | } 101 | 102 | protected getObject(instanceClass: INewable, instanceId: string | null | undefined): T { 103 | const argumentLength = instanceClass.prototype.constructor.length; 104 | if (argumentLength > 0) { 105 | let instanceKey = this._getInstanceKey(instanceClass, instanceId); 106 | let instance = this._instanceMap[instanceKey]; 107 | if (!instance) { 108 | throw new Error("Object requested is not created yet. Ensure that the object is created before it is queried. " + instanceClass); 109 | } 110 | else { 111 | return instance; 112 | } 113 | } 114 | else { 115 | return this.createObject(instanceClass, instanceId, null); 116 | } 117 | } 118 | 119 | protected removeObject(instanceClass: INewable, instanceId: string | null | undefined): void { 120 | let instanceKey = this._getInstanceKey(instanceClass, instanceId); 121 | this._deleteInstance(instanceKey); 122 | } 123 | 124 | protected createObject(instanceClass: INewable, instanceId: string | null | undefined, args: U | null): T { 125 | let instanceKey = this._getInstanceKey(instanceClass, instanceId); 126 | let instance = this._instanceMap[instanceKey]; 127 | if (!instance) { 128 | instance = Factory.createObject(instanceClass, null); 129 | this.onObjectCreated(instance, instanceId); 130 | this._instanceMap[instanceKey] = instance; 131 | } 132 | 133 | return instance; 134 | } 135 | 136 | protected abstract onObjectCreated(instance: T, instanceId: string | null | undefined): void; 137 | 138 | private _getInstanceKey(instanceClass: INewable, instanceId: string | null | undefined): string { 139 | let instanceKey: string = instanceClass.getKey(); 140 | if (instanceId) { 141 | instanceKey = instanceKey + "." + instanceId; 142 | } 143 | 144 | return instanceKey.toLowerCase(); 145 | } 146 | 147 | private _deleteInstance(instanceKey: string): void { 148 | let instance = this._instanceMap[instanceKey]; 149 | if (instance) { 150 | let disposeFunc = (instance).__dispose; 151 | if (disposeFunc && typeof disposeFunc === "function") { 152 | (instance).__dispose(); 153 | } 154 | 155 | delete this._instanceMap[instanceKey]; 156 | } 157 | } 158 | 159 | private _instanceMap: { [key: string]: T }; 160 | } 161 | 162 | export abstract class Manager extends BaseManager { 163 | 164 | protected onObjectCreated(instance: Initializable, instanceId: string | null | undefined) { 165 | instance.initialize(instanceId); 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/Store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | export interface IStoreState { } 7 | 8 | export interface IEventHandler extends Function { } 9 | 10 | /** 11 | * @brief Common class for base store 12 | */ 13 | export abstract class StoreBase { 14 | 15 | constructor(changedEvent?: string) { 16 | this.changedEvent = changedEvent || "DefaultChangeEvent"; 17 | this.handlers = {}; 18 | } 19 | 20 | /** 21 | * @brief This method returns an unique key for the store. The same will be used in StoreManager to store in the Dictionary 22 | */ 23 | public static getKey(): string { 24 | throw new Error("This method needs to be implemented in derived classes"); 25 | } 26 | 27 | /** 28 | * @brief Initializes the store 29 | */ 30 | public initialize(instanceId?: string): void { 31 | this._instanceId = instanceId || ""; 32 | } 33 | 34 | /** 35 | * @brief Returns the instanceId 36 | */ 37 | public getInstanceId(): string | null { 38 | return this._instanceId; 39 | } 40 | 41 | /** 42 | * @brief Returns the state information preserved in store 43 | */ 44 | public getState(): IStoreState { 45 | return {} as IStoreState; 46 | } 47 | 48 | public addChangedListener(handler: IEventHandler) { 49 | this.addListener(this.changedEvent, handler); 50 | } 51 | 52 | public removeChangedListener(handler: IEventHandler) { 53 | this.removeListener(this.changedEvent, handler); 54 | } 55 | 56 | public addListener(eventName: string, handler: IEventHandler): void { 57 | if (!this.handlers[eventName]) { 58 | this.handlers[eventName] = []; 59 | } 60 | this.handlers[eventName].push(handler); 61 | } 62 | 63 | public removeListener(eventName: string, handler: IEventHandler): void { 64 | if (this.handlers[eventName]) { 65 | for (let handlerIndex = this.handlers[eventName].length - 1; handlerIndex >= 0; handlerIndex--) { 66 | if (this.handlers[eventName][handlerIndex] === handler) { 67 | this.handlers[eventName].splice(handlerIndex, 1); 68 | } 69 | } 70 | } 71 | } 72 | 73 | protected emitChanged(): void { 74 | this.emit(this.changedEvent, this); 75 | } 76 | 77 | protected emit(eventName: string, sender: {}): void { 78 | if (this.handlers[eventName]) { 79 | for (const handler of this.handlers[eventName]) { 80 | handler(sender); 81 | } 82 | } 83 | } 84 | 85 | private _dispose(): void { 86 | this.disposeInternal(); 87 | this._instanceId = null; 88 | } 89 | 90 | protected abstract disposeInternal(): void; 91 | private _instanceId: string | null; 92 | private changedEvent: string; 93 | private handlers: { [eventName: string]: IEventHandler[] }; 94 | } 95 | -------------------------------------------------------------------------------- /src/WebUI/FluxCommon/StoreManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { Manager, INewable, Initializable } from "./Factory"; 7 | 8 | export class StoreManager extends Manager { 9 | 10 | /** 11 | * Get an instance of the Store. 12 | */ 13 | public static GetStore(storeClass: INewable, instanceId?: string): T { 14 | return super.getInstance(StoreManager).getObject(storeClass, instanceId) as T; 15 | } 16 | 17 | public static CreateStore(storeClass: INewable, instanceId: string, args: U): T { 18 | return super.getInstance(StoreManager).createObject(storeClass, instanceId, args) as T; 19 | } 20 | 21 | public static DeleteStore(storeClass: INewable, instanceId?: string): void { 22 | super.getInstance(StoreManager).removeObject(storeClass, instanceId); 23 | } 24 | 25 | public static dispose() { 26 | return super.getInstance(StoreManager).dispose(); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/WebUI/ImageDetails/ImageDetails.scss: -------------------------------------------------------------------------------- 1 | // following items are not needed ideally 2 | .list-col-content { 3 | &.two-lines { 4 | line-height: 30px; 5 | align-items: flex-start; 6 | width: 100%; 7 | } 8 | } 9 | 10 | .kube-status-container { 11 | display: flex; 12 | align-items: center; 13 | 14 | .kube-status-desc { 15 | margin-left: 6px; 16 | width: 100%; 17 | } 18 | } 19 | 20 | .item-top-padding { 21 | margin-top: 8px; 22 | } 23 | // end of above common css copied 24 | 25 | .image-details-content .image-details-header .image-details-back-button { 26 | cursor: pointer; 27 | } 28 | 29 | .image-details-card .image-full-details-table { 30 | padding-bottom: 16px; 31 | } 32 | 33 | .image-layers-card .bolt-header-default { 34 | padding-bottom: 12px; 35 | } -------------------------------------------------------------------------------- /src/WebUI/ImageDetails/ImageDetailsActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionsHubBase, Action } from "../FluxCommon/Actions"; 7 | import { IImageDetails } from "../../Contracts/Types"; 8 | 9 | export class ImageDetailsActions extends ActionsHubBase { 10 | public static getKey(): string { 11 | return "image-details-actions"; 12 | } 13 | 14 | public initialize(): void { 15 | this._setHasImageDetails = new Action<{ [key: string]: boolean } | undefined>(); 16 | this._setImageDetails = new Action(); 17 | } 18 | 19 | public get setHasImageDetails(): Action<{ [key: string]: boolean } | undefined> { 20 | return this._setHasImageDetails; 21 | } 22 | 23 | public get setImageDetails(): Action { 24 | return this._setImageDetails; 25 | } 26 | 27 | private _setHasImageDetails: Action<{ [key: string]: boolean } | undefined>; 28 | private _setImageDetails: Action; 29 | } 30 | -------------------------------------------------------------------------------- /src/WebUI/ImageDetails/ImageDetailsActionsCreator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionCreatorBase, Action } from "../FluxCommon/Actions"; 7 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 8 | import { StoreManager } from "../FluxCommon/StoreManager"; 9 | import { IImageService } from "../../Contracts/Contracts"; 10 | import { IImageDetails } from "../../Contracts/Types"; 11 | import { ImageDetailsActions } from "./ImageDetailsActions"; 12 | import { ImageDetailsStore } from "./ImageDetailsStore"; 13 | import { ImageDetailsEvents } from "../Constants"; 14 | import { KubeFactory } from "../KubeFactory"; 15 | 16 | export class ImageDetailsActionsCreator extends ActionCreatorBase { 17 | public static getKey(): string { 18 | return "image-details-actionscreator"; 19 | } 20 | 21 | public initialize(instanceId?: string): void { 22 | this._actions = ActionsHubManager.GetActionsHub(ImageDetailsActions); 23 | this._imageDetailsStore = StoreManager.GetStore(ImageDetailsStore); 24 | } 25 | 26 | public setHasImageDetails(imageService: IImageService, listImages: string[]): void { 27 | imageService && imageService.hasImageDetails(listImages).then(hasImageDetails => { 28 | if (hasImageDetails && hasImageDetails.hasOwnProperty("hasImageDetails")) { 29 | this._actions.setHasImageDetails.invoke(hasImageDetails["hasImageDetails"] as { [key: string]: boolean }); 30 | } 31 | }); 32 | } 33 | 34 | public getImageDetails(imageId: string, showImageDetailsAction: (imageDetails: IImageDetails) => void) { 35 | let imageDetails: IImageDetails | undefined = this._imageDetailsStore.getImageDetails(imageId); 36 | if (imageDetails) { 37 | showImageDetailsAction(imageDetails); 38 | } 39 | else { 40 | const imageService = KubeFactory.getImageService(); 41 | imageService && imageService.getImageDetails(imageId).then(fetchedImageDetails => { 42 | if (fetchedImageDetails) { 43 | this._actions.setImageDetails.invoke(fetchedImageDetails); 44 | showImageDetailsAction(fetchedImageDetails); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | private _actions: ImageDetailsActions; 51 | private _imageDetailsStore: ImageDetailsStore; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/WebUI/ImageDetails/ImageDetailsStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { StoreBase } from "../FluxCommon/Store"; 7 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 8 | import { IImageDetails } from "../../Contracts/Types"; 9 | import { ImageDetailsActions } from "./ImageDetailsActions"; 10 | import { ImageDetailsEvents } from "../Constants"; 11 | 12 | export class ImageDetailsStore extends StoreBase { 13 | public static getKey(): string { 14 | return "image-details-store"; 15 | } 16 | 17 | public initialize(instanceId?: string): void { 18 | super.initialize(instanceId); 19 | 20 | this._actions = ActionsHubManager.GetActionsHub(ImageDetailsActions); 21 | this._actions.setHasImageDetails.addListener(this._setHasImageDetailsData); 22 | this._actions.setImageDetails.addListener(this._setImageDetailsData); 23 | } 24 | 25 | public disposeInternal(): void { 26 | this._actions.setHasImageDetails.removeListener(this._setHasImageDetailsData); 27 | this._actions.setImageDetails.removeListener(this._setImageDetailsData); 28 | } 29 | 30 | public hasImageDetails(imageName: string): boolean | undefined { 31 | return this._hasImageDetails[imageName]; 32 | } 33 | 34 | public getImageDetails(imageName: string): IImageDetails | undefined { 35 | return this._imageDetails[imageName]; 36 | } 37 | 38 | private _setHasImageDetailsData = (payload: { [key: string]: boolean } | undefined): void => { 39 | if (payload) { 40 | this._hasImageDetails = payload; 41 | this.emit(ImageDetailsEvents.HasImageDetailsEvent, this); 42 | } 43 | } 44 | 45 | private _setImageDetailsData = (payload: IImageDetails): void => { 46 | if (payload && payload.imageName) { 47 | this._imageDetails[payload.imageName] = payload; 48 | this.emit(ImageDetailsEvents.SetImageDetailsEvent, this); 49 | } 50 | } 51 | 52 | private _hasImageDetails: { [key: string]: boolean } = {}; 53 | private _imageDetails: { [key: string]: IImageDetails } = {}; 54 | private _actions: ImageDetailsActions; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/WebUI/KubeFactory.ts: -------------------------------------------------------------------------------- 1 | import { IImageService, IKubeService, ITelemetryService, KubeImage } from "../Contracts/Contracts"; 2 | 3 | class DefaultTelemetryService implements ITelemetryService { 4 | public markTimeToInteractive(scenarioName: string, additionalProperties?: { [key: string]: any; } | undefined): void { 5 | console.log(`Scenario ready for interaction ${scenarioName}, properties:${JSON.stringify(additionalProperties || {})}`); 6 | } 7 | 8 | public onClickTelemetry(source: string, additionalProperties?: { [key: string]: any; }): void { 9 | console.log(`Item clicked ${source}, properties:${JSON.stringify(additionalProperties || {})}`); 10 | } 11 | 12 | scenarioStart(scenarioName: string, additionalProperties?: { [key: string]: any; }): void { 13 | console.log(`Scenario started ${scenarioName}, properties:${JSON.stringify(additionalProperties || {})}`); 14 | } 15 | 16 | scenarioEnd(scenarioName: string, additionalProperties?: { [key: string]: any; }): void { 17 | console.log(`Scenario completed ${scenarioName}, properties:${JSON.stringify(additionalProperties || {})}`); 18 | } 19 | } 20 | 21 | export class KubeFactory { 22 | public static setImageLocation(imageLocation?: (image: KubeImage) => string | undefined): void { 23 | KubeFactory._imageLocation = imageLocation; 24 | } 25 | 26 | public static getImageLocation(image: KubeImage): string | undefined { 27 | return KubeFactory._imageLocation ? KubeFactory._imageLocation(image) : ""; 28 | } 29 | 30 | public static markTTI = (scenarioName: string, additionalProperties?: { [key: string]: any; } | undefined) => { 31 | getTelemetryService().markTimeToInteractive(scenarioName, additionalProperties); 32 | } 33 | 34 | public static setTelemetryService(telemetryService?: ITelemetryService): void { 35 | if (telemetryService) { 36 | KubeFactory._telemetryService = telemetryService; 37 | } 38 | } 39 | 40 | public static getTelemetryService(): ITelemetryService { 41 | if (!KubeFactory._telemetryService) { 42 | KubeFactory._telemetryService = new DefaultTelemetryService(); 43 | } 44 | return KubeFactory._telemetryService; 45 | } 46 | 47 | public static setImageService(imageService: IImageService | undefined): void { 48 | KubeFactory._imageService = imageService; 49 | } 50 | 51 | public static getImageService(): IImageService | undefined { 52 | return KubeFactory._imageService; 53 | } 54 | 55 | public static setKubeService(kubeService: IKubeService): void { 56 | KubeFactory._kubeService = kubeService; 57 | } 58 | 59 | public static getKubeService(): IKubeService { 60 | return KubeFactory._kubeService; 61 | } 62 | 63 | private static _telemetryService?: ITelemetryService; 64 | private static _imageService: IImageService | undefined; 65 | private static _kubeService: IKubeService; 66 | private static _imageLocation?: (image: KubeImage) => string | undefined; 67 | } 68 | 69 | export function getTelemetryService(): ITelemetryService { 70 | return KubeFactory.getTelemetryService(); 71 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodContentReader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import * as React from "react"; 7 | import { getContentReaderComponent } from "../Common/KubeConsumer"; 8 | import { IVssComponentProperties } from "../Types"; 9 | import {Scenarios} from "../Constants"; 10 | 11 | export interface IPodReaderProps extends IVssComponentProperties { 12 | text: string; 13 | options?: any; /* monaco.editor.IEditorConstructionOptions */ 14 | contentClassName?: string; 15 | } 16 | 17 | export class PodContentReader extends React.Component { 18 | public render(): React.ReactNode { 19 | return getContentReaderComponent({ ...this.props }); 20 | } 21 | 22 | public componentDidMount() { 23 | this.props.markTTICallback && this.props.markTTICallback({ 24 | "scenario": Scenarios.PodYaml 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodLog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod } from "@kubernetes/client-node"; 7 | import * as Util_String from "azure-devops-ui/Core/Util/String"; 8 | import * as React from "react"; 9 | import * as Resources from "../../Resources"; 10 | import { Scenarios } from "../Constants"; 11 | import { KubeFactory } from "../KubeFactory"; 12 | import { PodContentReader } from "./PodContentReader"; 13 | import { IPodRightPanelProps } from "./Types"; 14 | 15 | export interface IPodLogProps extends IPodRightPanelProps { 16 | // Overriding this to make sure we don't accept undefined 17 | pod: V1Pod; 18 | } 19 | 20 | interface IPodLogState { 21 | logContent: string; 22 | uid: string; 23 | } 24 | 25 | export class PodLog extends React.Component { 26 | constructor(props: IPodLogProps) { 27 | super(props, {}); 28 | this.state = { logContent: Resources.LoadingText, uid: this.props.pod.metadata.uid }; 29 | } 30 | 31 | public render(): JSX.Element { 32 | return ( 33 | 45 | ); 46 | } 47 | 48 | public componentDidMount(): void { 49 | const service = KubeFactory.getKubeService(); 50 | const podName = this.props.pod.metadata.name; 51 | const spec = this.props.pod.spec || undefined; 52 | const podContainerName = spec && spec.containers && spec.containers.length > 0 && spec.containers[0].name || ""; 53 | 54 | const scenarioPayload = { 55 | "scenario": Scenarios.PodLogs 56 | }; 57 | service && service.getPodLog && service.getPodLog(podName, podContainerName).then(logContent => { 58 | this.setState({ 59 | uid: Util_String.newGuid(), // required to refresh the content 60 | logContent: logContent || "" 61 | }); 62 | this.props.markTTICallback && this.props.markTTICallback(scenarioPayload); 63 | }).catch(error => { 64 | let errorMessage = error || ""; 65 | errorMessage = (typeof errorMessage == "string") ? errorMessage : JSON.stringify(errorMessage); 66 | this.setState({ 67 | uid: Util_String.newGuid(), // required to refresh the content 68 | logContent: errorMessage 69 | }); 70 | this.props.markTTICallback && this.props.markTTICallback(scenarioPayload); 71 | }); 72 | } 73 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodOverview.scss: -------------------------------------------------------------------------------- 1 | .pod-overview-card .pod-full-details-table { 2 | padding-bottom: 16px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodOverview.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod } from "@kubernetes/client-node"; 7 | import { Ago } from "azure-devops-ui/Ago"; 8 | import { CardContent, CustomCard } from "azure-devops-ui/Card"; 9 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 10 | import { CustomHeader, HeaderTitle, HeaderTitleArea, HeaderTitleRow, TitleSize } from "azure-devops-ui/Header"; 11 | import { Link } from "azure-devops-ui/Link"; 12 | import { Tooltip } from "azure-devops-ui/TooltipEx"; 13 | import { css } from "azure-devops-ui/Util"; 14 | import { AgoFormat } from "azure-devops-ui/Utilities/Date"; 15 | import * as React from "react"; 16 | import * as Resources from "../../Resources"; 17 | import { defaultColumnRenderer } from "../Common/KubeCardWithTable"; 18 | import { Tags } from "../Common/Tags"; 19 | import { Scenarios } from "../Constants"; 20 | import { StoreManager } from "../FluxCommon/StoreManager"; 21 | import { ImageDetailsStore } from "../ImageDetails/ImageDetailsStore"; 22 | import { getRunDetailsText } from "../RunDetails"; 23 | import { Utils } from "../Utils"; 24 | import "./PodOverview.scss"; 25 | import { IPodRightPanelProps } from "./Types"; 26 | 27 | export interface IPodOverviewProps extends IPodRightPanelProps { 28 | // overriding this to make sure we don't accept undefined 29 | pod: V1Pod; 30 | showImageDetails?: (imageId: string) => void; 31 | } 32 | 33 | export class PodOverview extends React.Component { 34 | public render(): JSX.Element { 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | {Resources.PodDetailsHeader} 42 | 43 | 44 | 45 | 46 | 47 | {this._getCardContent()} 48 | 49 | 50 | ); 51 | } 52 | 53 | public componentDidMount(): void { 54 | this._markTTI(); 55 | } 56 | 57 | private _markTTI(): void { 58 | if (this.props.markTTICallback) { 59 | this.props.markTTICallback({ 60 | "scenario": Scenarios.PodOverview 61 | }); 62 | } 63 | } 64 | 65 | private static _getPodDetails = (pod: V1Pod, showImageDetails?: (imageId: string) => void): any[] => { 66 | const createTime = pod.metadata.creationTimestamp ? new Date(pod.metadata.creationTimestamp) : new Date().getTime(); 67 | const statusReason = pod.status.reason ? localeFormat(" | {0}", pod.status.reason) : ""; 68 | const statusText = localeFormat("{0}{1}", pod.status.phase, statusReason); 69 | const hasAnnotations = pod.metadata.annotations && Object.keys(pod.metadata.annotations).length > 0; 70 | const hasLabels = pod.metadata.labels && Object.keys(pod.metadata.labels).length > 0; 71 | const { imageText, imageTooltipText } = Utils.getImageText(pod.spec); 72 | const imageId: string = Utils.getImageIdsForPods([pod])[0] || ""; 73 | const conditionsText = PodOverview._getPodConditionsText(pod); 74 | const jobName = getRunDetailsText(pod.metadata.annotations); 75 | let podDetails: any[] = []; 76 | // order of rows to be preserved as per spec 77 | createTime && podDetails.push({ key: Resources.Created, value: createTime }); 78 | jobName && podDetails.push({ key: Resources.JobText, value: jobName }); 79 | hasAnnotations && podDetails.push({ key: Resources.AnnotationsText, value: pod.metadata.annotations }); 80 | pod.spec.restartPolicy && podDetails.push({ key: Resources.RestartPolicyText, value: pod.spec.restartPolicy }); 81 | pod.status.qosClass && podDetails.push({ key: Resources.QoSClassText, value: pod.status.qosClass }); 82 | pod.spec.nodeName && podDetails.push({ key: Resources.NodeText, value: pod.spec.nodeName }); 83 | imageText && podDetails.push({ key: Resources.ImageText, value: imageText, valueTooltipText: imageTooltipText, imageId: imageId, showImageDetails: showImageDetails }); 84 | hasLabels && podDetails.push({ key: Resources.LabelsText, value: pod.metadata.labels }); 85 | statusText && podDetails.push({ key: Resources.StatusText, value: statusText }); 86 | conditionsText && podDetails.push({ key: Resources.ConditionsText, value: conditionsText }); 87 | 88 | return podDetails; 89 | } 90 | 91 | private static _getPodConditionsText(pod: V1Pod): string { 92 | let conditions: string[] = []; 93 | if (pod.status) { 94 | conditions = (pod.status.conditions || []).map(condition => localeFormat("{0}={1}", condition.type || "", condition.status || "")); 95 | } 96 | 97 | return conditions.join("; ") || ""; 98 | } 99 | 100 | private static _renderValueCell = (tableItem: any) => { 101 | const { key, value, valueTooltipText } = tableItem; 102 | switch (key) { 103 | case Resources.Created: 104 | return ( 105 |
106 | 107 |
108 | ); 109 | 110 | case Resources.LabelsText: 111 | case Resources.AnnotationsText: 112 | return ( 113 |
114 | 115 |
116 | ); 117 | 118 | default: 119 | return defaultColumnRenderer(value, "details-card-value-field-size", valueTooltipText); 120 | } 121 | } 122 | 123 | private _getCardContent = (): JSX.Element => { 124 | const items = PodOverview._getPodDetails(this.props.pod, this.props.showImageDetails); 125 | const rowClassNames = "flex-row details-card-row-size"; 126 | const keyClassNames = "text-ellipsis secondary-text details-card-info-field-size"; 127 | return ( 128 |
129 | {items.map((item, index) => 130 | (item.key === Resources.ImageText) 131 | ? ( 132 |
133 |
134 | {item.key} 135 |
136 | {PodOverview._renderImageCell(item)} 137 |
138 | ) 139 | : ( 140 |
141 |
142 | {item.key} 143 |
144 | {PodOverview._renderValueCell(item)} 145 |
146 | ) 147 | )} 148 |
149 | ); 150 | } 151 | 152 | private static _renderImageCell = (tableItem: any) => { 153 | const { key, value, valueTooltipText, imageId, showImageDetails } = tableItem; 154 | const imageDetailsStore = StoreManager.GetStore(ImageDetailsStore); 155 | let imageDetailsUnavailableTooltipText = ""; 156 | const hasImageDetails: boolean | undefined = imageDetailsStore.hasImageDetails(imageId); 157 | // if hasImageDetails is undefined, then image details promise has not resolved, so do not set imageDetailsUnavailable tooltip 158 | if (hasImageDetails === false) { 159 | imageDetailsUnavailableTooltipText = localeFormat("{0} | {1}", valueTooltipText || value, Resources.ImageDetailsUnavailableText); 160 | } 161 | 162 | return hasImageDetails ? 163 | 164 |
165 | { 169 | e.preventDefault(); 170 | showImageDetails(imageId); 171 | }} 172 | > 173 | {value} 174 | 175 |
176 |
177 | : defaultColumnRenderer(value, "pod-image-nolink details-card-value-field-size", imageDetailsUnavailableTooltipText); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/WebUI/Pods/PodYaml.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod } from "@kubernetes/client-node"; 7 | import * as JsYaml from "js-yaml"; 8 | import * as React from "react"; 9 | import { PodContentReader } from "./PodContentReader"; 10 | import { IPodRightPanelProps } from "./Types"; 11 | 12 | export interface IPodYamlProps extends IPodRightPanelProps { 13 | // Overriding this to make sure we don't accept undefined 14 | pod: V1Pod; 15 | } 16 | 17 | export class PodYaml extends React.Component { 18 | public render(): JSX.Element { 19 | return ( 20 | 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionsHubBase, Action } from "../FluxCommon/Actions"; 7 | import { V1PodList } from "@kubernetes/client-node"; 8 | 9 | export interface IPodsPayload { 10 | podsList: V1PodList; 11 | isLoading: boolean; 12 | } 13 | 14 | export interface IPodListWithLabel extends IPodsPayload { 15 | labelSelector: string; 16 | } 17 | 18 | export class PodsActions extends ActionsHubBase { 19 | public static getKey(): string { 20 | return "pods-actions"; 21 | } 22 | 23 | public initialize(): void { 24 | this._podsFetched = new Action(); 25 | this._podsFetchedByLabel = new Action(); 26 | } 27 | 28 | public get podsFetched(): Action { 29 | return this._podsFetched; 30 | } 31 | 32 | public get podsFetchedByLabel(): Action { 33 | return this._podsFetchedByLabel; 34 | } 35 | 36 | private _podsFetched: Action; 37 | private _podsFetchedByLabel: Action; 38 | } 39 | -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsActionsCreator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod, V1PodList, V1ListMeta } from "@kubernetes/client-node"; 7 | import { IKubeService } from "../../Contracts/Contracts"; 8 | import { ActionCreatorBase } from "../FluxCommon/Actions"; 9 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 10 | import { PodsActions } from "./PodsActions"; 11 | 12 | export class PodsActionsCreator extends ActionCreatorBase { 13 | public static getKey(): string { 14 | return "pods-actionscreator"; 15 | } 16 | 17 | public initialize(instanceId?: string): void { 18 | this._actions = ActionsHubManager.GetActionsHub(PodsActions); 19 | } 20 | 21 | public getPods(kubeService: IKubeService, labelSelector?: string, fetchByLabel: boolean = false): void { 22 | if (fetchByLabel && !labelSelector) { 23 | // For service's associated pods, fetchByLabel is true; In this scenario if no label selector is supplied, it implies zero pods 24 | const podList: V1PodList = { 25 | apiVersion: "", 26 | items: [], 27 | kind: "", 28 | metadata: {} as V1ListMeta 29 | }; 30 | // Calling action inside timeout to ensure the action listener is initialized in the ServiceDetails 31 | setTimeout(() => this._actions.podsFetchedByLabel.invoke({ podsList: podList, labelSelector: "", isLoading: false })); 32 | } 33 | else { 34 | kubeService && kubeService.getPods(labelSelector || undefined).then(podList => { 35 | this._extendPodMetadataInList(podList); 36 | if (labelSelector) { 37 | this._actions.podsFetchedByLabel.invoke({ podsList: podList, labelSelector: labelSelector, isLoading: false }); 38 | } 39 | else { 40 | this._actions.podsFetched.invoke({ podsList: podList, isLoading: false }); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | // getPod() will provide apiVersion for pod also, should we call? 47 | private _extendPodMetadataInList(podList: V1PodList): void { 48 | const pods: V1Pod[] = podList && podList.items || []; 49 | const podsCount: number = pods.length; 50 | for (let i: number = 0; i < podsCount; i++) { 51 | const pod: V1Pod = podList.items[i]; 52 | podList.items[i] = { 53 | apiVersion: pod.apiVersion || podList.apiVersion, 54 | kind: pod.kind || "Pod", 55 | ...pod 56 | }; 57 | } 58 | } 59 | 60 | private _actions: PodsActions; 61 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsLeftPanel.scss: -------------------------------------------------------------------------------- 1 | @import "azure-devops-ui/_coreStyles.scss"; 2 | 3 | .pods-left-panel-row:not(.first-row) .bolt-list-cell { 4 | // This is required as we are doing a custom implementation of list row, and the border is not set by default 5 | border-top: 1px solid $transblack-8; 6 | } 7 | 8 | .pods-left-panel-row { 9 | cursor: pointer; 10 | } 11 | 12 | .pod-text-status-container { 13 | padding: 10px 0px; 14 | 15 | .pod-noshrink { 16 | // width is set in order to align the status icons vertically with the default back button in master panel header 17 | width: 23px; 18 | } 19 | 20 | .pod-shrink { 21 | min-width: 0px; 22 | } 23 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsLeftPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod } from "@kubernetes/client-node"; 7 | import { IListItemDetails, IListSelection, List, ListItem, ListSelection } from "azure-devops-ui/List"; 8 | import { Status, StatusSize } from "azure-devops-ui/Status"; 9 | import { ITableRow } from "azure-devops-ui/Table"; 10 | import { Tooltip } from "azure-devops-ui/TooltipEx"; 11 | import { css } from "azure-devops-ui/Util"; 12 | import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider"; 13 | import { createBrowserHistory } from "history"; 14 | import * as React from "react"; 15 | import * as queryString from "simple-query-string"; 16 | import { IVssComponentProperties } from "../Types"; 17 | import { Utils } from "../Utils"; 18 | import "./PodsLeftPanel.scss"; 19 | 20 | export interface IPodsLeftPanelProperties extends IVssComponentProperties { 21 | pods: V1Pod[]; 22 | parentName: string; 23 | selectedPodName?: string; 24 | parentKind: string; 25 | onSelectionChange?: (event: React.SyntheticEvent, selectedItem: V1Pod, selectedView: string) => void; 26 | } 27 | 28 | export class PodsLeftPanel extends React.Component { 29 | public render(): React.ReactNode { 30 | return ( 31 | this.props.pods && this.props.pods.length > 0 ? 32 | (this.props.pods)} 34 | onSelect={this._onSelectionChange} 35 | selection={this._selection} 36 | renderRow={this._renderListRow} 37 | width="100%" 38 | /> 39 | : null 40 | ); 41 | } 42 | 43 | public componentDidMount(): void { 44 | if (this.props.pods && this.props.pods.length) { 45 | this._selectPod(); 46 | } 47 | } 48 | 49 | public componentDidUpdate(prevProps: IPodsLeftPanelProperties): void { 50 | if (!(prevProps.pods && prevProps.pods.length) && (this.props.pods && this.props.pods.length)) { 51 | this._selectPod(); 52 | } 53 | } 54 | 55 | private _onSelectionChange = (event: React.SyntheticEvent, tableRow: ITableRow) => { 56 | this._selectedRow = tableRow.index; 57 | const historyService = createBrowserHistory(); 58 | const queryParams = queryString.parse(historyService.location.search); 59 | const selectedView = queryParams["view"]; 60 | if (this.props.onSelectionChange) { 61 | this.props.onSelectionChange(event, this.props.pods[tableRow.index], (typeof selectedView === "string") ? selectedView : ""); 62 | } 63 | } 64 | 65 | 66 | private _renderListRow = (index: number, pod: V1Pod, details: IListItemDetails, key?: string): JSX.Element => { 67 | const { statusProps, tooltip } = Utils.generatePodStatusProps(pod.status); 68 | const rowClassName = index === 0 ? css("pods-left-panel-row", "first-row") : "pods-left-panel-row"; 69 | const textClassName = css("primary-text text-ellipsis body-m", index === this._selectedRow ? "font-weight-semibold" : ""); 70 | 71 | return ( 72 | 73 |
74 |
75 |
76 | { 77 | statusProps && 78 | 79 |
80 | 81 |
82 |
83 | } 84 | 85 |
{pod.metadata.name}
86 |
87 |
88 |
89 | 90 | ); 91 | } 92 | 93 | private _selectPod(): void { 94 | const selectedPodName = this.props.selectedPodName; 95 | if (!this._hasSelected) { 96 | if (selectedPodName) { 97 | this._selectedRow = (this.props.pods || []).findIndex(pod => pod.metadata.name === selectedPodName); 98 | } 99 | 100 | if (this._selectedRow === -1 || this._selectedRow >= (this.props.pods || []).length) { 101 | this._selectedRow = 0; 102 | } 103 | 104 | // select the first pod in left panel by default 105 | this._selection.select(this._selectedRow); 106 | this._hasSelected = true; 107 | } 108 | } 109 | 110 | private _selection: IListSelection = new ListSelection({ selectOnFocus: true }); 111 | private _hasSelected: boolean = false; 112 | private _selectedRow: number = -1; 113 | } 114 | -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsRightPanel.scss: -------------------------------------------------------------------------------- 1 | .pod-overview-full-size { 2 | height: 100%; 3 | } 4 | 5 | .pod-overview-content-full-size { 6 | min-height: 100%; 7 | } 8 | 9 | .pod-overview-error-height { 10 | min-height: calc(100% - 64px); 11 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { StoreBase } from "../FluxCommon/Store"; 7 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 8 | import { V1PodList, V1Pod } from "@kubernetes/client-node"; 9 | import { PodsActions, IPodListWithLabel, IPodsPayload } from "./PodsActions"; 10 | import { PodsEvents } from "../Constants"; 11 | 12 | export interface IPodsStoreState { 13 | podsList?: V1PodList; 14 | isLoading?: boolean; 15 | podListByLabel: { [label: string]: V1PodList }; 16 | } 17 | 18 | export class PodsStore extends StoreBase { 19 | public static getKey(): string { 20 | return "pods-store"; 21 | } 22 | 23 | public initialize(instanceId?: string): void { 24 | super.initialize(instanceId); 25 | 26 | this._state = { podsList: undefined, podListByLabel: {}, isLoading: true }; 27 | 28 | this._actions = ActionsHubManager.GetActionsHub(PodsActions); 29 | this._actions.podsFetched.addListener(this._setPodsList); 30 | this._actions.podsFetchedByLabel.addListener(this._setPodsListByLabel); 31 | } 32 | 33 | public disposeInternal(): void { 34 | this._actions.podsFetched.removeListener(this._setPodsList); 35 | this._actions.podsFetchedByLabel.removeListener(this._setPodsListByLabel); 36 | } 37 | 38 | public getState(): IPodsStoreState { 39 | return this._state; 40 | } 41 | 42 | private _setPodsList = (payload: IPodsPayload): void => { 43 | this._state.podsList = payload.podsList; 44 | this._state.isLoading = payload.isLoading; 45 | this.emit(PodsEvents.PodsFetchedEvent, this); 46 | } 47 | 48 | private _setPodsListByLabel = (payload: IPodListWithLabel): void => { 49 | this._state.podListByLabel[payload.labelSelector] = payload.podsList; 50 | this._state.isLoading = payload.isLoading; 51 | this.emit(PodsEvents.LabelledPodsFetchedEvent, this); 52 | } 53 | 54 | private _state: IPodsStoreState; 55 | private _actions: PodsActions; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/WebUI/Pods/PodsTable.scss: -------------------------------------------------------------------------------- 1 | .pods-associated .bolt-header-default { 2 | padding-bottom: 12px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Pods/Types.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod } from "@kubernetes/client-node"; 7 | import { IStatusProps } from "azure-devops-ui/Status"; 8 | import { IVssComponentProperties } from "../Types"; 9 | 10 | export interface IPodRightPanelProps extends IVssComponentProperties { 11 | pod: V1Pod | undefined; 12 | podUid?: string; 13 | podStatusProps?: IStatusProps; 14 | statusTooltip?: string; 15 | showImageDetails?: (imageId: string) => void; 16 | notifyTabChange?: () => void; 17 | } -------------------------------------------------------------------------------- /src/WebUI/RunDetails.scss: -------------------------------------------------------------------------------- 1 | .runs-bullet { 2 | margin: 0 6px; 3 | } 4 | 5 | .run-name-link.bolt-table-link{ 6 | padding: 0; 7 | } -------------------------------------------------------------------------------- /src/WebUI/RunDetails.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 7 | import { Link } from "azure-devops-ui/Link"; 8 | import * as React from "react"; 9 | import * as Resources from "../Resources"; 10 | import "./RunDetails.scss"; 11 | import { IMetadataAnnotationPipeline, Utils } from "./Utils"; 12 | 13 | export function getRunDetailsText(annotations?: { [key: string]: string }, jobAndPipelineDetails?: IMetadataAnnotationPipeline, createdAgo?: string): React.ReactNode { 14 | let pipelineDetails: IMetadataAnnotationPipeline = {} as IMetadataAnnotationPipeline; 15 | if (jobAndPipelineDetails) { 16 | pipelineDetails = jobAndPipelineDetails; 17 | } 18 | else if (annotations) { 19 | pipelineDetails = Utils.getPipelineDetails(annotations); 20 | } 21 | else { 22 | return ""; 23 | } 24 | 25 | if (!createdAgo && !pipelineDetails.jobName) { 26 | return null; 27 | } 28 | 29 | const trimmedJobName = pipelineDetails.jobName && pipelineDetails.jobName.replace(/^"|"$/g, ""); 30 | const first = createdAgo 31 | ? (trimmedJobName 32 | ? localeFormat(Resources.ServiceCreatedWithPipelineText, createdAgo, trimmedJobName) 33 | : localeFormat(Resources.CreatedAgo, createdAgo)) 34 | : trimmedJobName; 35 | 36 | const runElement = pipelineDetails.runName 37 | ? (pipelineDetails.runUrl 38 | ? ( {"#" + pipelineDetails.runName} ) 39 | : "#" + pipelineDetails.runName) 40 | : undefined; 41 | 42 | let second = runElement; 43 | if (pipelineDetails.pipelineName) { 44 | // Trying to format the resource string with {0} possibly being a link. 45 | // First put the pipeline name, and in the place on run, put {0} 46 | // Pipeline name might have surrounding quotes 47 | const runText = localeFormat(Resources.RunInformationForWorkload, "{0}", pipelineDetails.pipelineName.replace(/^"|"$/g, "")); 48 | 49 | // Find index of the run placeholder 50 | const indexOfRun = runText.indexOf("{0}"); 51 | 52 | second = (<> 53 | {runText.substring(0, indexOfRun - 1)} 54 | {runElement} 55 | {runText.substring(indexOfRun + 3)} 56 | ); 57 | } 58 | 59 | return ( 60 | <> 61 | {first} 62 | { 63 | second 64 | ? <> 65 | 66 | {second} 67 | 68 | : undefined 69 | } 70 | 71 | ) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/WebUI/Selection/SelectionActionCreator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { createBrowserHistory } from "history"; 7 | import * as queryString from "simple-query-string"; 8 | import { ActionCreatorBase } from "../FluxCommon/Actions"; 9 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 10 | import { ISelectionPayload, SelectionActions } from "./SelectionActions"; 11 | 12 | export class SelectionActionsCreator extends ActionCreatorBase { 13 | 14 | public static getKey(): string { 15 | return "selection-actionscreator"; 16 | } 17 | 18 | public initialize(instanceId?: string): void { 19 | this._actions = ActionsHubManager.GetActionsHub(SelectionActions); 20 | } 21 | 22 | public selectItem(payload: ISelectionPayload): void { 23 | // Create history service fresh here, because we need the fresh url location 24 | // Unlike components, action creators are not re initialized on view mount 25 | const historyService = createBrowserHistory(); 26 | let routeValues = { ...queryString.parse(historyService.location.search) }; 27 | routeValues["type"] = payload.selectedItemType; 28 | routeValues["uid"] = payload.itemUID; 29 | 30 | if (!!payload.selectedItemType) { 31 | delete routeValues["view"]; 32 | } 33 | 34 | // This logic needs some refining. We need to plan a course of action that will suit an open source hosting. The host might have its own query parameters 35 | // So, our design should be careful in handling those 36 | if (payload.properties) { 37 | // Add all the non-object, non-function and defined values to the url route. This will be read by the view loading itself 38 | Object.keys(payload.properties).forEach(pk => { 39 | const pValue = payload.properties![pk]; 40 | // do assign value of 0 or false 41 | // delete the key which has no value 42 | if (pValue !== undefined && pValue !== null && typeof pValue !== "object" && typeof pValue !== "function" && pValue !== "") { 43 | routeValues[pk] = pValue; 44 | } 45 | else { 46 | delete routeValues[pk]; 47 | } 48 | }); 49 | } 50 | 51 | Object.keys(routeValues).forEach(rk => { 52 | const paramValue = routeValues[rk]; 53 | // delete the properties which has no value 54 | // do not delete param value with 0 or false 55 | if (paramValue === undefined || paramValue === null || paramValue === "") { 56 | delete routeValues[rk]; 57 | } 58 | }); 59 | 60 | historyService.push({ 61 | pathname: historyService.location.pathname, 62 | search: queryString.stringify(routeValues) 63 | }); 64 | 65 | this._actions.selectItem.invoke(payload); 66 | } 67 | 68 | private _actions: SelectionActions; 69 | } 70 | -------------------------------------------------------------------------------- /src/WebUI/Selection/SelectionActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionsHubBase, Action } from "../FluxCommon/Actions"; 7 | import { V1ReplicaSet, V1Pod, V1DaemonSet, V1StatefulSet } from "@kubernetes/client-node"; 8 | import { IServiceItem } from "../Types"; 9 | import { IImageDetails } from "../../Contracts/Types"; 10 | 11 | export interface ISelectionPayload { 12 | item: V1ReplicaSet | V1DaemonSet | V1StatefulSet | IServiceItem | V1Pod | IImageDetails | undefined; 13 | itemUID: string; 14 | showSelectedItem: boolean; 15 | selectedItemType: string; 16 | properties?: { [key: string]: any }; 17 | } 18 | 19 | export class SelectionActions extends ActionsHubBase { 20 | public static getKey(): string { 21 | return "kubernetes-selection-actions"; 22 | } 23 | 24 | public initialize(): void { 25 | this._selectItem = new Action(); 26 | } 27 | 28 | 29 | public get selectItem(): Action { 30 | return this._selectItem; 31 | } 32 | 33 | private _selectItem: Action; 34 | } 35 | -------------------------------------------------------------------------------- /src/WebUI/Selection/SelectionStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1DaemonSet, V1Pod, V1ReplicaSet, V1StatefulSet } from "@kubernetes/client-node"; 7 | import { IImageDetails } from "../../Contracts/Types"; 8 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 9 | import { StoreBase } from "../FluxCommon/Store"; 10 | import { IServiceItem } from "../Types"; 11 | import { ISelectionPayload, SelectionActions } from "./SelectionActions"; 12 | 13 | export interface ISelectionStoreState { 14 | selectedItem: V1ReplicaSet | V1DaemonSet | V1StatefulSet | IServiceItem | V1Pod | IImageDetails | undefined; 15 | itemUID: string; 16 | showSelectedItem: boolean; 17 | selectedItemType: string; 18 | properties?: { [key: string]: string }; 19 | } 20 | 21 | export class SelectionStore extends StoreBase { 22 | public static getKey(): string { 23 | return "kubernetes-selection-store"; 24 | } 25 | 26 | public initialize(instanceId?: string): void { 27 | super.initialize(instanceId); 28 | 29 | this._state = { selectedItem: undefined, showSelectedItem: false, selectedItemType: "", itemUID: "" }; 30 | 31 | this._actions = ActionsHubManager.GetActionsHub(SelectionActions); 32 | this._actions.selectItem.addListener(this._select); 33 | } 34 | 35 | public disposeInternal(): void { 36 | this._actions.selectItem.removeListener(this._select); 37 | } 38 | 39 | public getState(): ISelectionStoreState { 40 | return this._state; 41 | } 42 | 43 | private _select = (payload: ISelectionPayload): void => { 44 | this._state.selectedItem = payload.item; 45 | this._state.showSelectedItem = payload.showSelectedItem; 46 | this._state.selectedItemType = payload.selectedItemType; 47 | this._state.itemUID = payload.itemUID; 48 | this._state.properties = payload.properties; 49 | this.emitChanged(); 50 | } 51 | 52 | private _state: ISelectionStoreState; 53 | private _actions: SelectionActions; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/WebUI/Services/ServiceDetails.scss: -------------------------------------------------------------------------------- 1 | .service-full-details-table { 2 | padding-top: 8px; 3 | padding-bottom: 20px; 4 | width: 100%; 5 | overflow-x: auto; 6 | } 7 | 8 | .service-column-key-padding { 9 | padding-bottom: 4px; 10 | } 11 | 12 | .service-card-content { 13 | width: 100%; 14 | 15 | .service-column-padding { 16 | margin-left: 24px; 17 | } 18 | } 19 | 20 | .service-basic-column-size { 21 | width: 104px; 22 | min-width: 104px; 23 | } 24 | 25 | .service-extip-column-size { 26 | width: 124px; 27 | } 28 | 29 | .service-tags-column-size { 30 | min-width: 150px; 31 | } -------------------------------------------------------------------------------- /src/WebUI/Services/ServiceUtils.ts: -------------------------------------------------------------------------------- 1 | import { V1Service, V1ServicePort } from "@kubernetes/client-node"; 2 | import { IServiceItem } from "../Types"; 3 | import { Utils } from "../Utils"; 4 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 5 | 6 | export function getServiceItems(serviceList: V1Service[]): IServiceItem[] { 7 | let items: IServiceItem[] = []; 8 | serviceList.forEach(service => { 9 | items.push({ 10 | package: service.metadata.name, 11 | type: service.spec.type, 12 | clusterIP: service.spec.clusterIP || "-", 13 | externalIP: _getExternalIP(service), 14 | port: _getPort(service) || "", 15 | creationTimestamp: service.metadata.creationTimestamp || new Date(), 16 | uid: service.metadata.uid.toLowerCase(), 17 | pipeline: Utils.getPipelineText(service.metadata.annotations), 18 | service: service, 19 | kind: service.kind || "Service" 20 | }); 21 | }); 22 | 23 | return items; 24 | } 25 | 26 | function _getPort(service: V1Service): string { 27 | if (service.spec 28 | && service.spec.ports 29 | && service.spec.ports.length > 0) { 30 | const ports = service.spec.ports.map(port => _formatPortString(port)); 31 | return ports.join(", "); 32 | } 33 | 34 | return ""; 35 | } 36 | 37 | function _formatPortString(servicePort: V1ServicePort): string { 38 | const nodePort = servicePort.nodePort ? ":" + servicePort.nodePort : ""; 39 | // example: 80:2080/TCP, if nodeport. 80/TCP, if no nodeport 40 | return localeFormat("{0}{1}/{2}", servicePort.port, nodePort, servicePort.protocol); 41 | } 42 | 43 | 44 | function _getExternalIP(service: V1Service): string { 45 | return service.status 46 | && service.status.loadBalancer 47 | && service.status.loadBalancer.ingress 48 | && service.status.loadBalancer.ingress.length > 0 49 | && service.status.loadBalancer.ingress[0].ip 50 | || ""; 51 | } 52 | -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionsHubBase, Action } from "../FluxCommon/Actions"; 7 | import { V1ServiceList } from "@kubernetes/client-node"; 8 | 9 | export class ServicesActions extends ActionsHubBase { 10 | public static getKey(): string { 11 | return "services-actions"; 12 | } 13 | 14 | public initialize(): void { 15 | this._servicesFetched = new Action(); 16 | } 17 | 18 | public get servicesFetched(): Action { 19 | return this._servicesFetched; 20 | } 21 | 22 | private _servicesFetched: Action; 23 | } 24 | -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesActionsCreator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ActionCreatorBase, Action } from "../FluxCommon/Actions"; 7 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 8 | import { IKubeService } from "../../Contracts/Contracts"; 9 | import { ServicesActions } from "./ServicesActions"; 10 | 11 | export class ServicesActionsCreator extends ActionCreatorBase { 12 | public static getKey(): string { 13 | return "services-actionscreator"; 14 | } 15 | 16 | public initialize(instanceId?: string): void { 17 | this._actions = ActionsHubManager.GetActionsHub(ServicesActions); 18 | } 19 | 20 | public getServices(kubeService: IKubeService): void { 21 | kubeService.getServices().then(servicesList => { 22 | this._actions.servicesFetched.invoke(servicesList); 23 | }); 24 | } 25 | 26 | private _actions: ServicesActions; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesFilterBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1ServiceList } from "@kubernetes/client-node"; 7 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 8 | import { IListSelection, ListSelection } from "azure-devops-ui/List"; 9 | import { IListBoxItem } from "azure-devops-ui/ListBox"; 10 | import { Filter } from "azure-devops-ui/Utilities/Filter"; 11 | import * as React from "react"; 12 | import * as Resources from "../../Resources"; 13 | import { KubeFilterBar } from "../Common/KubeFilterBar"; 14 | import { IVssComponentProperties } from "../Types"; 15 | 16 | export interface IServiceFilterBarProps extends IVssComponentProperties { 17 | filter: Filter; 18 | filterToggled: ObservableValue; 19 | serviceList: V1ServiceList; 20 | } 21 | 22 | export class ServicesFilterBar extends React.Component { 23 | public render(): React.ReactNode { 24 | const svcTypes = this._generateSvcTypes(); 25 | let items: IListBoxItem<{}>[] = []; 26 | for (const svc of svcTypes) { 27 | let item = { 28 | id: svc, 29 | text: svc 30 | }; 31 | items.push(item); 32 | }; 33 | 34 | return ( 35 | 44 | ); 45 | } 46 | 47 | private _generateSvcTypes(): string[] { 48 | let svcTypes: string[] = []; 49 | this.props.serviceList && this.props.serviceList.items && this.props.serviceList.items.forEach((svc) => { 50 | if (svcTypes.indexOf(svc.spec.type) === -1) { 51 | svcTypes.push(svc.spec.type); 52 | } 53 | }); 54 | 55 | return svcTypes; 56 | } 57 | 58 | private _selection: IListSelection = new ListSelection(true); 59 | } -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesPivot.scss: -------------------------------------------------------------------------------- 1 | .loading-services { 2 | height: 200px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesPivot.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1ServiceList } from "@kubernetes/client-node"; 7 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 8 | import { Spinner, SpinnerSize } from "azure-devops-ui/Spinner"; 9 | import { css } from "azure-devops-ui/Util"; 10 | import { Filter, IFilterItemState, IFilterState } from "azure-devops-ui/Utilities/Filter"; 11 | import * as React from "react"; 12 | import * as Resources from "../../Resources"; 13 | import { NameKey, TypeKey } from "../Common/KubeFilterBar"; 14 | import { KubeZeroData } from "../Common/KubeZeroData"; 15 | import { Scenarios, ServicesEvents } from "../Constants"; 16 | import { ActionsCreatorManager } from "../FluxCommon/ActionsCreatorManager"; 17 | import { StoreManager } from "../FluxCommon/StoreManager"; 18 | import { getTelemetryService, KubeFactory } from "../KubeFactory"; 19 | import { ServicesTable } from "../Services/ServicesTable"; 20 | import { IVssComponentProperties } from "../Types"; 21 | import { ServicesActionsCreator } from "./ServicesActionsCreator"; 22 | import { ServicesFilterBar } from "./ServicesFilterBar"; 23 | import "./ServicesPivot.scss"; 24 | import { ServicesStore } from "./ServicesStore"; 25 | 26 | export interface IServicesPivotState { 27 | serviceList?: V1ServiceList; 28 | isLoading?: boolean; 29 | } 30 | 31 | export interface IServicesPivotProps extends IVssComponentProperties { 32 | filter: Filter; 33 | namespace?: string; 34 | filterToggled: ObservableValue; 35 | } 36 | 37 | export class ServicesPivot extends React.Component { 38 | constructor(props: IServicesPivotProps) { 39 | super(props, {}); 40 | 41 | getTelemetryService().scenarioStart(Scenarios.Services); 42 | 43 | this._actionCreator = ActionsCreatorManager.GetActionCreator(ServicesActionsCreator); 44 | this._store = StoreManager.GetStore(ServicesStore); 45 | 46 | const storeState = this._store.getState(); 47 | this.state = { 48 | serviceList: storeState.serviceList, 49 | isLoading: storeState.isLoading 50 | }; 51 | 52 | this._actionCreator.getServices(KubeFactory.getKubeService()); 53 | this._store.addListener(ServicesEvents.ServicesFetchedEvent, this._onServicesFetched); 54 | } 55 | 56 | public render(): React.ReactNode { 57 | if (this.state.isLoading) { 58 | return ; 59 | } 60 | 61 | return ( 62 | <> 63 | {this._getFilterBar()} 64 |
65 | {this._getContent()} 66 |
67 | 68 | ); 69 | } 70 | 71 | public componentWillUnmount(): void { 72 | this._store.removeListener(ServicesEvents.ServicesFetchedEvent, this._onServicesFetched); 73 | } 74 | 75 | private _onServicesFetched = (): void => { 76 | const storeState = this._store.getState(); 77 | this.setState({ 78 | serviceList: storeState.serviceList, 79 | isLoading: storeState.isLoading 80 | }); 81 | } 82 | 83 | private _getContent(): JSX.Element { 84 | const serviceSize: number = this.state.serviceList && this.state.serviceList.items ? this.state.serviceList.items.length : 0; 85 | return (serviceSize === 0 ? this._getZeroData() : 86 | ); 92 | } 93 | 94 | private _getFilterBar(): JSX.Element { 95 | return (); 101 | } 102 | 103 | private _getNameFilterValue(): string | undefined { 104 | const filterState: IFilterState | undefined = this.props.filter.getState(); 105 | const filterItem: IFilterItemState | null = filterState ? filterState[NameKey] : null; 106 | return filterItem ? (filterItem.value as string) : undefined; 107 | } 108 | 109 | private _getTypeFilterValue(): any[] { 110 | const filterState: IFilterState | undefined = this.props.filter.getState(); 111 | const filterItem: IFilterItemState | null = filterState ? filterState[TypeKey] : null; 112 | const selections: any[] = filterItem ? filterItem.value : []; 113 | return selections; 114 | } 115 | 116 | private _getZeroData(): JSX.Element { 117 | setTimeout(this._markTTI, 0); 118 | return KubeZeroData.getServicesZeroData(); 119 | } 120 | 121 | private _markTTI = () => { 122 | if (!this._isTTIMarked) { 123 | getTelemetryService().scenarioEnd(Scenarios.Services); 124 | } 125 | this._isTTIMarked = true; 126 | } 127 | 128 | private _isTTIMarked: boolean = false; 129 | private _store: ServicesStore; 130 | private _actionCreator: ServicesActionsCreator; 131 | } -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Pod, V1ServiceList } from "@kubernetes/client-node"; 7 | import { ServicesEvents } from "../Constants"; 8 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 9 | import { StoreBase } from "../FluxCommon/Store"; 10 | import { IPodListWithLabel, PodsActions } from "../Pods/PodsActions"; 11 | import { ServicesActions } from "./ServicesActions"; 12 | 13 | export interface IServicesStoreState { 14 | serviceList?: V1ServiceList 15 | podsList?: V1Pod[]; 16 | isLoading?: boolean; 17 | arePodsLoading?: boolean; 18 | } 19 | 20 | export class ServicesStore extends StoreBase { 21 | public static getKey(): string { 22 | return "services-store"; 23 | } 24 | 25 | public initialize(instanceId?: string): void { 26 | super.initialize(instanceId); 27 | 28 | this._state = { serviceList: undefined, podsList: [], isLoading: true, arePodsLoading: true }; 29 | 30 | this._servicesActions = ActionsHubManager.GetActionsHub(ServicesActions); 31 | this._podsActions = ActionsHubManager.GetActionsHub(PodsActions); 32 | 33 | this._servicesActions.servicesFetched.addListener(this._servicesFetched); 34 | this._podsActions.podsFetchedByLabel.addListener(this._setAssociatedPodsList); 35 | } 36 | 37 | public disposeInternal(): void { 38 | this._servicesActions.servicesFetched.removeListener(this._servicesFetched); 39 | this._podsActions.podsFetchedByLabel.removeListener(this._setAssociatedPodsList); 40 | } 41 | 42 | public getState(): IServicesStoreState { 43 | return this._state; 44 | } 45 | 46 | public getServicesSize(): number { 47 | return this._state.serviceList && this._state.serviceList.items ? this._state.serviceList.items.length : 0; 48 | } 49 | 50 | private _servicesFetched = (serviceList: V1ServiceList): void => { 51 | this._state.serviceList = serviceList; 52 | this._state.isLoading = false; 53 | this.emit(ServicesEvents.ServicesFetchedEvent, this); 54 | if (this._state.serviceList && this._state.serviceList.items && this._state.serviceList.items.length > 0) { 55 | this.emit(ServicesEvents.ServicesFoundEvent, this); 56 | } 57 | } 58 | 59 | private _setAssociatedPodsList = (payload: IPodListWithLabel): void => { 60 | this._state.podsList = payload.podsList && payload.podsList.items; 61 | this._state.arePodsLoading = payload.isLoading; 62 | this.emit(ServicesEvents.ServicePodsFetchedEvent, this); 63 | } 64 | 65 | private _state: IServicesStoreState; 66 | private _servicesActions: ServicesActions; 67 | private _podsActions: PodsActions; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/WebUI/Services/ServicesTable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1Service, V1ServiceList } from "@kubernetes/client-node"; 7 | import { Ago } from "azure-devops-ui/Ago"; 8 | import { Card } from "azure-devops-ui/Card"; 9 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 10 | import { ScreenBreakpoints } from "azure-devops-ui/Core/Util/Screen"; 11 | import * as Utils_Accessibility from "azure-devops-ui/Core/Util/Accessibility"; 12 | import { IStatusProps, Status, Statuses, StatusSize } from "azure-devops-ui/Status"; 13 | import { ITableColumn, ITableRow, renderSimpleCell, Table, TwoLineTableCell, ITableProps } from "azure-devops-ui/Table"; 14 | import { Tooltip } from "azure-devops-ui/TooltipEx"; 15 | import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider"; 16 | import * as React from "react"; 17 | import * as Resources from "../../Resources"; 18 | import { renderExternalIpCell, renderTableCell } from "../Common/KubeCardWithTable"; 19 | import { KubeZeroData } from "../Common/KubeZeroData"; 20 | import { SelectedItemKeys } from "../Constants"; 21 | import { ActionsCreatorManager } from "../FluxCommon/ActionsCreatorManager"; 22 | import { SelectionActionsCreator } from "../Selection/SelectionActionCreator"; 23 | import { ISelectionPayload } from "../Selection/SelectionActions"; 24 | import { IServiceItem, IVssComponentProperties } from "../Types"; 25 | import { Utils } from "../Utils"; 26 | import "./ServiceDetails.scss"; 27 | import { getServiceItems } from "./ServiceUtils"; 28 | 29 | const loadBalancerKey: string = "LoadBalancer"; 30 | 31 | export interface IServicesComponentProperties extends IVssComponentProperties { 32 | typeSelections: string[]; 33 | serviceList: V1ServiceList; 34 | nameFilter?: string; 35 | } 36 | 37 | export interface IServicesTableState { 38 | copiedRowIndex: number; 39 | } 40 | 41 | export class ServicesTable extends React.Component { 42 | constructor(props: IServicesComponentProperties) { 43 | super(props, {}); 44 | 45 | this.state = { 46 | copiedRowIndex: -1 47 | }; 48 | 49 | this._selectionActionCreator = ActionsCreatorManager.GetActionCreator(SelectionActionsCreator); 50 | } 51 | 52 | public render(): React.ReactNode { 53 | const filteredSvc: V1Service[] = (this.props.serviceList && this.props.serviceList.items || []) 54 | .filter((svc) => { 55 | return this._filterService(svc); 56 | }); 57 | 58 | if (filteredSvc.length > 0) { 59 | const serviceItems = getServiceItems(filteredSvc).map((item, index) => { 60 | item.externalIPTooltip = index === this.state.copiedRowIndex ? Resources.CopiedExternalIp : Resources.CopyExternalIp; 61 | return item; 62 | }); 63 | const tableProps = { 64 | id: "services-list-table", 65 | role: "table", 66 | showHeader: true, 67 | showLines: true, 68 | singleClickActivation: true, 69 | itemProvider: new ArrayItemProvider(serviceItems), 70 | ariaLabel: Resources.PivotServiceText, 71 | columns: this._columns, 72 | tableBreakpoints: [{ 73 | breakpoint: ScreenBreakpoints.xsmall, 74 | columnWidths: [-100, 0, 0, 0, 0] 75 | }, { 76 | breakpoint: ScreenBreakpoints.small, 77 | columnWidths: [-100, 0, 175, 200, 0] 78 | }, { 79 | breakpoint: ScreenBreakpoints.medium, 80 | columnWidths: [-70, 100, 100, 100, -40] 81 | }], 82 | onActivate: (event: React.SyntheticEvent, tableRow: ITableRow) => { 83 | this._openServiceItem(event, tableRow, serviceItems[tableRow.index]); 84 | } 85 | } as ITableProps; 86 | return ( 87 | 89 | 92 | 93 | ); 94 | } else { 95 | return KubeZeroData.getNoResultsZeroData(); 96 | } 97 | } 98 | 99 | public componentDidMount(): void { 100 | this.props.markTTICallback && this.props.markTTICallback(); 101 | } 102 | 103 | private _openServiceItem = (event: React.SyntheticEvent, tableRow: ITableRow, selectedItem: IServiceItem) => { 104 | if (selectedItem) { 105 | const payload: ISelectionPayload = { 106 | item: selectedItem, 107 | itemUID: (selectedItem.service as V1Service).metadata.uid, 108 | showSelectedItem: true, 109 | selectedItemType: SelectedItemKeys.ServiceItemKey 110 | }; 111 | 112 | this._selectionActionCreator.selectItem(payload); 113 | } 114 | } 115 | 116 | private _renderPackageKeyCell = (rowIndex: number, columnIndex: number, tableColumn: ITableColumn, service: IServiceItem): JSX.Element => { 117 | return ServicesTable._getServiceStatusWithName(service, columnIndex, tableColumn); 118 | } 119 | 120 | private _renderAgeCell = (rowIndex: number, columnIndex: number, tableColumn: ITableColumn, service: IServiceItem): JSX.Element => { 121 | const itemToRender = ; 122 | return renderTableCell(rowIndex, columnIndex, tableColumn, itemToRender); 123 | } 124 | 125 | private _setCopiedRowIndex = (copiedRowIndex: number): void => { 126 | this.setState({ 127 | copiedRowIndex: copiedRowIndex 128 | }, () => { 129 | copiedRowIndex !== -1 && Utils_Accessibility.announce(Resources.CopiedExternalIp); 130 | }); 131 | } 132 | 133 | private static _getServiceStatusWithName(service: IServiceItem, columnIndex: number, tableColumn: ITableColumn): JSX.Element { 134 | let statusProps: IStatusProps = Statuses.Success; 135 | let tooltipText: string = ""; 136 | if (service.type === loadBalancerKey) { 137 | tooltipText = Resources.SucceededText; 138 | if (!service.externalIP) { 139 | tooltipText = Resources.InProgressText; 140 | statusProps = Statuses.Running; 141 | } 142 | } 143 | 144 | return ( 145 | 151 |
{service.package}
152 | 153 | } 154 | line2={ 155 |
{service.type}
156 | } 157 | iconProps={{ 158 | render: (className?: string) => { 159 | return ( 160 | 161 |
162 | 163 |
164 |
165 | ); 166 | } 167 | }} 168 | /> 169 | ); 170 | } 171 | 172 | private _filterService(svc: V1Service): boolean { 173 | const nameMatches: boolean = Utils.filterByName(svc.metadata.name, this.props.nameFilter); 174 | const typeMatches: boolean = this.props.typeSelections.length > 0 ? this.props.typeSelections.indexOf(svc.spec.type) >= 0 : true; 175 | 176 | return nameMatches && typeMatches; 177 | } 178 | 179 | private _columns = [ 180 | // negative widths are interpreted as percentages. 181 | // since we want the table columns to occupy full available width, setting width - 100 which is equivalent to 100 % 182 | { 183 | id: "package", 184 | name: Resources.NameText, 185 | width: new ObservableValue(-70), 186 | renderCell: this._renderPackageKeyCell 187 | 188 | }, 189 | { 190 | id: "clusterIP", 191 | name: Resources.ClusterIPText, 192 | width: new ObservableValue(-15), 193 | renderCell: renderSimpleCell 194 | }, 195 | { 196 | id: "externalIP", 197 | name: Resources.ExternalIPText, 198 | width: new ObservableValue(172), 199 | renderCell: (rowIndex, columnIndex, tableColumn, service) => renderExternalIpCell(rowIndex, columnIndex, tableColumn, service, this._setCopiedRowIndex) 200 | }, 201 | { 202 | id: "port", 203 | name: Resources.PortText, 204 | width: new ObservableValue(200), 205 | renderCell: renderSimpleCell 206 | }, 207 | { 208 | id: "creationTimestamp", 209 | name: Resources.AgeText, 210 | width: new ObservableValue(-15), 211 | renderCell: this._renderAgeCell 212 | } 213 | ]; 214 | 215 | private _selectionActionCreator: SelectionActionsCreator; 216 | } 217 | -------------------------------------------------------------------------------- /src/WebUI/Types.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1DaemonSet, V1DaemonSetList, V1Deployment, V1DeploymentList, V1PodList, V1ReplicaSet, V1ReplicaSetList, V1Service, V1ServiceList, V1StatefulSet, V1StatefulSetList, V1Pod } from "@kubernetes/client-node"; 7 | import { IObservable } from "azure-devops-ui/Core/Observable"; 8 | import { IStatusProps } from "azure-devops-ui/Status"; 9 | 10 | interface IBaseProps { 11 | componentRef?: (ref: T | null) => (void | T); 12 | } 13 | 14 | export interface IKubernetesSummary { 15 | namespace?: string; 16 | podList?: V1PodList; 17 | deploymentList?: V1DeploymentList; 18 | serviceList?: V1ServiceList; 19 | replicaSetList?: V1ReplicaSetList; 20 | daemonSetList?: V1DaemonSetList; 21 | statefulSetList?: V1StatefulSetList; 22 | } 23 | 24 | export interface IDeploymentReplicaSetItem { 25 | name?: string; 26 | replicaSetName?: string; 27 | deploymentId?: string; 28 | replicaSetId?: string; 29 | pipeline?: string; 30 | pods?: string; 31 | podsTooltip?: string; 32 | statusProps?: IStatusProps; 33 | showRowBorder?: boolean; 34 | deployment?: V1Deployment; 35 | imageId: string; 36 | imageDisplayText: string; 37 | imageTooltip?: string; 38 | creationTimeStamp: Date; 39 | kind?: string; 40 | } 41 | 42 | export interface IServiceItem { 43 | package: string; 44 | type: string; 45 | clusterIP: string; 46 | externalIP: string; 47 | port: string; 48 | creationTimestamp: Date; 49 | uid: string; 50 | pipeline: string; 51 | service?: V1Service; 52 | kind?: string; 53 | externalIPTooltip?: string; 54 | } 55 | 56 | export interface IDeploymentReplicaSetMap { 57 | deployment: V1Deployment; 58 | //this list is sorted in descending order 59 | replicaSets: V1ReplicaSet[]; 60 | } 61 | 62 | export interface ISetWorkloadTypeItem { 63 | name: string; 64 | uid: string; 65 | kind: string; 66 | imageId: string; 67 | imageDisplayText: string; 68 | imageTooltip?: string; 69 | desiredPodCount: number; 70 | currentPodCount: number; 71 | creationTimeStamp: Date; 72 | payload: V1DaemonSet | V1StatefulSet | V1ReplicaSet | V1Pod; 73 | statusProps?: IStatusProps; 74 | statusTooltip?: string; 75 | } 76 | 77 | export interface IPodDetailsSelectionProperties { 78 | parentUid: string; 79 | serviceSelector?: string; 80 | serviceName?: string; 81 | } 82 | 83 | export interface IVssComponentProperties extends IBaseProps { 84 | /** 85 | * Components may specify a css classe list that should be applied to the primary 86 | * element of the component when it is rendered. 87 | */ 88 | className?: string; 89 | 90 | /** 91 | * Components MAY specify an order value which is a number > 0 which defines its 92 | * rendering order when multiple components target the same componentRegion. If the 93 | * order is NOT specified it defaults to Number.MAX_VALUE. 94 | */ 95 | componentOrder?: number; 96 | 97 | /** 98 | * Key value for this component that MUST be set when the component is rendered 99 | * into a set of components. 100 | */ 101 | key?: string | number; 102 | 103 | /** 104 | * Mark TTI callback for child components 105 | */ 106 | markTTICallback?: (additionalProperties?: { [key: string]: any }) => void; 107 | 108 | /** 109 | * Any of the properties MAY be accessed as an IObservable. 110 | */ 111 | [property: string]: IObservable | any; 112 | } 113 | 114 | export interface IPodParentItem { 115 | name: string; 116 | kind: string; 117 | } -------------------------------------------------------------------------------- /src/WebUI/Workloads/DeploymentsTable.scss: -------------------------------------------------------------------------------- 1 | .deployment-tbl-heading-labels { 2 | padding-left: 8px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadDetails.scss: -------------------------------------------------------------------------------- 1 | .workload-full-details-table { 2 | padding-top: 24px; 3 | padding-bottom: 20px; 4 | width: 100%; 5 | overflow-x: auto; 6 | } 7 | 8 | // devops special override 9 | .bolt-header-default+.workload-full-details-table { 10 | padding-top: 8px; 11 | } 12 | 13 | .workload-column-key-padding { 14 | padding-bottom: 4px; 15 | } 16 | 17 | .workload-tags-column-padding { 18 | margin-left: 24px; 19 | min-width: 150px; 20 | } 21 | 22 | .workload-card-content { 23 | width: 100%; 24 | 25 | .workload-image-padding { 26 | padding: 0px; 27 | } 28 | } 29 | 30 | .workload-image-column-size { 31 | width: 300px; 32 | min-width: 300px; 33 | } 34 | 35 | .workload-tags-column-size { 36 | min-width: 150px; 37 | } -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1DaemonSetList, V1DeploymentList, V1PodList, V1ReplicaSetList, V1StatefulSetList } from "@kubernetes/client-node"; 7 | import { Action, ActionsHubBase } from "../FluxCommon/Actions"; 8 | 9 | export class WorkloadsActions extends ActionsHubBase { 10 | public static getKey(): string { 11 | return "workloads-actions"; 12 | } 13 | 14 | public initialize(): void { 15 | this._deploymentsFetched = new Action(); 16 | this._replicaSetsFetched = new Action(); 17 | this._daemonSetsFetched = new Action(); 18 | this._statefulSetsFetched = new Action(); 19 | this._podsFetched = new Action(); 20 | } 21 | 22 | public get deploymentsFetched(): Action { 23 | return this._deploymentsFetched; 24 | } 25 | 26 | public get replicaSetsFetched(): Action { 27 | return this._replicaSetsFetched; 28 | } 29 | 30 | public get daemonSetsFetched(): Action { 31 | return this._daemonSetsFetched; 32 | } 33 | 34 | public get statefulSetsFetched(): Action { 35 | return this._statefulSetsFetched; 36 | } 37 | 38 | public get podsFetched(): Action { 39 | return this._podsFetched; 40 | } 41 | 42 | private _deploymentsFetched: Action; 43 | private _replicaSetsFetched: Action; 44 | private _daemonSetsFetched: Action; 45 | private _statefulSetsFetched: Action; 46 | private _podsFetched: Action; 47 | } 48 | -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsActionsCreator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { IKubeService } from "../../Contracts/Contracts"; 7 | import { ActionCreatorBase } from "../FluxCommon/Actions"; 8 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 9 | import { WorkloadsActions } from "./WorkloadsActions"; 10 | 11 | export class WorkloadsActionsCreator extends ActionCreatorBase { 12 | public static getKey(): string { 13 | return "workloads-actionscreator"; 14 | } 15 | 16 | public initialize(instanceId?: string): void { 17 | this._actions = ActionsHubManager.GetActionsHub(WorkloadsActions); 18 | } 19 | 20 | public getDeployments(kubeService: IKubeService): void { 21 | kubeService.getDeployments().then(deploymentList => { 22 | this._actions.deploymentsFetched.invoke(deploymentList); 23 | }); 24 | } 25 | 26 | public getReplicaSets(kubeService: IKubeService): void { 27 | kubeService.getReplicaSets().then(replicaSetsList => { 28 | this._actions.replicaSetsFetched.invoke(replicaSetsList); 29 | }); 30 | } 31 | 32 | public getDaemonSets(kubeService: IKubeService): void { 33 | kubeService.getDaemonSets().then(daemonSetsList => { 34 | this._actions.daemonSetsFetched.invoke(daemonSetsList); 35 | }); 36 | } 37 | 38 | public getStatefulSets(kubeService: IKubeService): void { 39 | kubeService.getStatefulSets().then(statefulSetsList => { 40 | this._actions.statefulSetsFetched.invoke(statefulSetsList); 41 | }); 42 | } 43 | 44 | public getPods(kubeService: IKubeService): void { 45 | kubeService.getPods().then(podsList => { 46 | this._actions.podsFetched.invoke(podsList); 47 | }); 48 | } 49 | 50 | private _actions: WorkloadsActions; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsFilterBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 7 | import { IListSelection, ListSelection } from "azure-devops-ui/List"; 8 | import { IListBoxItem } from "azure-devops-ui/ListBox"; 9 | import { Filter } from "azure-devops-ui/Utilities/Filter"; 10 | import * as React from "react"; 11 | import { KubeResourceType } from "../../Contracts/KubeServiceBase"; 12 | import * as Resources from "../../Resources"; 13 | import { KubeFilterBar } from "../Common/KubeFilterBar"; 14 | import { IVssComponentProperties } from "../Types"; 15 | 16 | export interface IWorkloadsFilterBarProps extends IVssComponentProperties { 17 | filter: Filter; 18 | filterToggled: ObservableValue; 19 | } 20 | 21 | export class WorkloadsFilterBar extends React.Component { 22 | public render(): React.ReactNode { 23 | const items: IListBoxItem<{}>[] = [ 24 | { id: KubeResourceType.Deployments.toString(), text: Resources.DeploymentsDetailsText }, 25 | { id: KubeResourceType.ReplicaSets.toString(), text: Resources.ReplicaSetText }, 26 | { id: KubeResourceType.DaemonSets.toString(), text: Resources.DaemonSetText }, 27 | { id: KubeResourceType.StatefulSets.toString(), text: Resources.StatefulSetText } 28 | ]; 29 | 30 | return (); 39 | } 40 | 41 | private _selection: IListSelection = new ListSelection(true); 42 | 43 | } -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsPivot.scss: -------------------------------------------------------------------------------- 1 | .workloads-pivot-data .bolt-header-default { 2 | padding-bottom: 12px; 3 | } -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsPivot.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1PodList } from "@kubernetes/client-node"; 7 | import { ObservableValue } from "azure-devops-ui/Core/Observable"; 8 | import { format } from "azure-devops-ui/Core/Util/String"; 9 | import { css } from "azure-devops-ui/Util"; 10 | import { Filter, IFilterItemState, IFilterState } from "azure-devops-ui/Utilities/Filter"; 11 | import * as React from "react"; 12 | import { KubeResourceType } from "../../Contracts/KubeServiceBase"; 13 | import { NameKey, TypeKey } from "../Common/KubeFilterBar"; 14 | import "../Common/KubeSummary.scss"; 15 | import { KubeZeroData } from "../Common/KubeZeroData"; 16 | import { PodsEvents, Scenarios, WorkloadsEvents } from "../Constants"; 17 | import { ActionsCreatorManager } from "../FluxCommon/ActionsCreatorManager"; 18 | import { StoreManager } from "../FluxCommon/StoreManager"; 19 | import { ImageDetailsActionsCreator } from "../ImageDetails/ImageDetailsActionsCreator"; 20 | import { getTelemetryService, KubeFactory } from "../KubeFactory"; 21 | import { PodsActionsCreator } from "../Pods/PodsActionsCreator"; 22 | import { PodsStore } from "../Pods/PodsStore"; 23 | import { IVssComponentProperties } from "../Types"; 24 | import { Utils } from "../Utils"; 25 | import { DeploymentsTable } from "./DeploymentsTable"; 26 | import { OtherWorkloads } from "./OtherWorkloadsTable"; 27 | import { WorkloadsActionsCreator } from "./WorkloadsActionsCreator"; 28 | import { WorkloadsFilterBar } from "./WorkloadsFilterBar"; 29 | import "./WorkloadsPivot.scss"; 30 | import { WorkloadsStore } from "./WorkloadsStore"; 31 | 32 | export interface IWorkloadsPivotState { 33 | workloadResourceSize: number; 34 | imageList: string[]; 35 | } 36 | 37 | export interface IWorkloadsPivotProps extends IVssComponentProperties { 38 | filter: Filter; 39 | namespace?: string; 40 | filterToggled: ObservableValue; 41 | } 42 | 43 | export class WorkloadsPivot extends React.Component { 44 | constructor(props: IWorkloadsPivotProps) { 45 | super(props, {}); 46 | getTelemetryService().scenarioStart(Scenarios.Workloads); 47 | this._workloadsStore = StoreManager.GetStore(WorkloadsStore); 48 | // initialize pods store as pods list will be required in workloadPodsView on item selection 49 | this._podsStore = StoreManager.GetStore(PodsStore); 50 | 51 | this._podsActionCreator = ActionsCreatorManager.GetActionCreator(PodsActionsCreator); 52 | this._imageActionsCreator = ActionsCreatorManager.GetActionCreator(ImageDetailsActionsCreator); 53 | this._workloadsActionCreator = ActionsCreatorManager.GetActionCreator(WorkloadsActionsCreator); 54 | 55 | this.state = { 56 | workloadResourceSize: 0, 57 | imageList: [] 58 | }; 59 | 60 | this._podsStore.addListener(PodsEvents.PodsFetchedEvent, this._onPodsFetched); 61 | this._workloadsStore.addListener(WorkloadsEvents.WorkloadsFoundEvent, this._onDataFound); 62 | 63 | const kubeService = KubeFactory.getKubeService(); 64 | this._workloadsActionCreator.getDeployments(kubeService); 65 | // fetch all pods in parent component as the podList is required in selected workload pods view 66 | this._podsActionCreator.getPods(kubeService); 67 | } 68 | 69 | public render(): React.ReactNode { 70 | return ( 71 | <> 72 | {this._getFilterBar()} 73 |
74 | {this._getContent()} 75 |
76 | 77 | ); 78 | } 79 | 80 | public componentWillUnmount(): void { 81 | this._workloadsStore.removeListener(WorkloadsEvents.WorkloadsFoundEvent, this._onDataFound); 82 | this._podsStore.removeListener(PodsEvents.PodsFetchedEvent, this._onPodsFetched); 83 | } 84 | 85 | public componentDidUpdate(prevProps: IWorkloadsPivotProps, prevState: IWorkloadsPivotState) { 86 | const imageService = KubeFactory.getImageService(); 87 | imageService && (this.state.imageList.length > 0) && this._imageActionsCreator.setHasImageDetails(imageService, this.state.imageList); 88 | } 89 | 90 | private _onPodsFetched = (): void => { 91 | const podlist: V1PodList | undefined = this._podsStore.getState().podsList; 92 | if (podlist && podlist.items && podlist.items.length > 0) { 93 | const imageList = Utils.getImageIdsForPods(podlist.items); 94 | this.setState({ 95 | imageList: imageList 96 | }); 97 | } 98 | } 99 | 100 | private _onDataFound = (): void => { 101 | const workloadSize = this._workloadsStore.getWorkloadSize(); 102 | if (this.state.workloadResourceSize <= 0 && workloadSize > 0) { 103 | this.setState({ workloadResourceSize: workloadSize }, () => { 104 | this._componentsInitialized = { 105 | "DeploymentTable": false, 106 | "OtherWorkloads": false 107 | } 108 | }); 109 | } 110 | } 111 | 112 | private _notifyRender = (props?: { [key: string]: string }) => { 113 | if (!this._isTTIMarked && props && this._componentsInitialized) { 114 | props["component"] ? this._componentsInitialized[props["component"]] = true : undefined; 115 | let initialized = true; 116 | Object.keys(this._componentsInitialized).forEach(key => initialized = initialized && this._componentsInitialized[key]) 117 | if (initialized && !this._isTTIMarked) { 118 | getTelemetryService().scenarioEnd(Scenarios.Workloads); 119 | this._isTTIMarked = true; 120 | } 121 | } 122 | } 123 | 124 | private _getContent(): JSX.Element { 125 | return (this.state.workloadResourceSize === 0 ? this._getZeroData() : 126 | <> 127 | {this._showComponent(KubeResourceType.Deployments) && this._getDeployments()} 128 | {this._getOtherWorkloadsComponent()} 129 | ); 130 | } 131 | 132 | private _getFilterBar(): JSX.Element { 133 | return ( 134 | 139 | ); 140 | } 141 | 142 | private _getOtherWorkloadsComponent(): JSX.Element { 143 | return (); 149 | } 150 | 151 | private _getDeployments(): JSX.Element { 152 | return (); 157 | } 158 | 159 | private _getNameFilterValue(): string | undefined { 160 | const filterState: IFilterState | undefined = this.props.filter.getState(); 161 | const filterItem: IFilterItemState | null = filterState ? filterState[NameKey] : null; 162 | return filterItem ? (filterItem.value as string) : undefined; 163 | } 164 | 165 | private _getTypeFilterValue(): any[] { 166 | const filterState: IFilterState | undefined = this.props.filter.getState(); 167 | const filterItem: IFilterItemState | null = filterState ? filterState[TypeKey] : null; 168 | const selections: any[] = filterItem ? filterItem.value : []; 169 | return selections; 170 | } 171 | 172 | private _showComponent(resourceType: KubeResourceType): boolean { 173 | const selections: string[] = this._getTypeFilterValue(); 174 | // if no filter selections are made, show all components 175 | if (selections.length > 0 && resourceType != undefined) { 176 | const selection = resourceType.toString(); 177 | return selections.indexOf(selection) != -1; 178 | } 179 | 180 | return true; 181 | } 182 | 183 | private _getZeroData(): JSX.Element { 184 | return KubeZeroData.getWorkloadsZeroData(); 185 | } 186 | 187 | private _isTTIMarked: boolean = false; 188 | private _workloadsStore: WorkloadsStore; 189 | private _workloadsActionCreator: WorkloadsActionsCreator; 190 | private _podsActionCreator: PodsActionsCreator; 191 | private _podsStore: PodsStore; 192 | private _imageActionsCreator: ImageDetailsActionsCreator; 193 | private _componentsInitialized: { [key: string]: boolean}; 194 | } 195 | -------------------------------------------------------------------------------- /src/WebUI/Workloads/WorkloadsStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT license. 4 | */ 5 | 6 | import { V1DaemonSetList, V1DeploymentList, V1Pod, V1ReplicaSetList, V1StatefulSetList } from "@kubernetes/client-node"; 7 | import { WorkloadsEvents } from "../Constants"; 8 | import { ActionsHubManager } from "../FluxCommon/ActionsHubManager"; 9 | import { StoreBase } from "../FluxCommon/Store"; 10 | import { IPodsPayload, PodsActions } from "../Pods/PodsActions"; 11 | import { WorkloadsActions } from "./WorkloadsActions"; 12 | 13 | export interface IWorkloadsStoreState { 14 | deploymentNamespace?: string; 15 | deploymentList?: V1DeploymentList; 16 | replicaSetList?: V1ReplicaSetList; 17 | daemonSetList?: V1DaemonSetList; 18 | statefulSetList?: V1StatefulSetList; 19 | orphanPodsList?: V1Pod[]; 20 | } 21 | 22 | export class WorkloadsStore extends StoreBase { 23 | public static getKey(): string { 24 | return "workloads-store"; 25 | } 26 | 27 | public initialize(instanceId?: string): void { 28 | super.initialize(instanceId); 29 | 30 | this._state = { deploymentNamespace: "", deploymentList: undefined, replicaSetList: undefined, daemonSetList: undefined, statefulSetList: undefined, orphanPodsList: [] }; 31 | 32 | this._workloadActions = ActionsHubManager.GetActionsHub(WorkloadsActions); 33 | this._podsActions = ActionsHubManager.GetActionsHub(PodsActions); 34 | 35 | this._workloadActions.deploymentsFetched.addListener(this._setDeploymentsList); 36 | this._workloadActions.replicaSetsFetched.addListener(this._setReplicaSetsList); 37 | this._workloadActions.daemonSetsFetched.addListener(this._setDaemonSetsList); 38 | this._workloadActions.statefulSetsFetched.addListener(this._setStatefulsetsList); 39 | this._podsActions.podsFetched.addListener(this._setOrphanPodsList); 40 | } 41 | 42 | public disposeInternal(): void { 43 | this._workloadActions.deploymentsFetched.removeListener(this._setDeploymentsList); 44 | this._workloadActions.replicaSetsFetched.removeListener(this._setReplicaSetsList); 45 | this._workloadActions.daemonSetsFetched.removeListener(this._setDaemonSetsList); 46 | this._workloadActions.statefulSetsFetched.removeListener(this._setStatefulsetsList); 47 | this._podsActions.podsFetched.removeListener(this._setOrphanPodsList); 48 | } 49 | 50 | public getState(): IWorkloadsStoreState { 51 | return this._state; 52 | } 53 | 54 | public getWorkloadSize(): number { 55 | return (this._state.deploymentList ? (this._state.deploymentList.items || []).length : 0) + 56 | (this._state.replicaSetList ? (this._state.replicaSetList.items || []).length : 0) + 57 | (this._state.daemonSetList ? (this._state.daemonSetList.items || []).length : 0) + 58 | (this._state.statefulSetList ? (this._state.statefulSetList.items || []).length : 0) + 59 | (this._state.orphanPodsList ? (this._state.orphanPodsList || []).length : 0); 60 | } 61 | 62 | private _setDeploymentsList = (deploymentsList: V1DeploymentList): void => { 63 | this._state.deploymentList = deploymentsList; 64 | const deploymentItems = deploymentsList ? deploymentsList.items || [] : []; 65 | for (const deployment of deploymentItems) { 66 | if (deployment && deployment.metadata.namespace) { 67 | this._state.deploymentNamespace = deployment.metadata.namespace; 68 | break; 69 | } 70 | } 71 | 72 | this.emit(WorkloadsEvents.DeploymentsFetchedEvent, this); 73 | 74 | if (this._state.deploymentList && this._state.deploymentList.items && this._state.deploymentList.items.length > 0) { 75 | this.emit(WorkloadsEvents.WorkloadsFoundEvent, this); 76 | } 77 | else { 78 | this.emit(WorkloadsEvents.ZeroDeploymentsFoundEvent, this); 79 | } 80 | } 81 | 82 | private _setReplicaSetsList = (replicaSetList: V1ReplicaSetList): void => { 83 | this._state.replicaSetList = replicaSetList; 84 | this.emit(WorkloadsEvents.ReplicaSetsFetchedEvent, this); 85 | if (this._state.replicaSetList && this._state.replicaSetList.items && this._state.replicaSetList.items.length > 0) { 86 | this.emit(WorkloadsEvents.WorkloadsFoundEvent, this); 87 | } 88 | } 89 | 90 | private _setDaemonSetsList = (daemonSetList: V1DaemonSetList): void => { 91 | this._state.daemonSetList = daemonSetList; 92 | this.emit(WorkloadsEvents.DaemonSetsFetchedEvent, this); 93 | if (this._state.daemonSetList && this._state.daemonSetList.items && this._state.daemonSetList.items.length > 0) { 94 | this.emit(WorkloadsEvents.WorkloadsFoundEvent, this); 95 | } 96 | } 97 | 98 | private _setStatefulsetsList = (statefulSetList: V1StatefulSetList): void => { 99 | this._state.statefulSetList = statefulSetList; 100 | this.emit(WorkloadsEvents.StatefulSetsFetchedEvent, this); 101 | if (this._state.statefulSetList && this._state.statefulSetList.items && this._state.statefulSetList.items.length > 0) { 102 | this.emit(WorkloadsEvents.WorkloadsFoundEvent, this); 103 | } 104 | } 105 | 106 | private _setOrphanPodsList = (payload: IPodsPayload): void => { 107 | let orphanPods: V1Pod[] = []; 108 | payload.podsList && payload.podsList.items && payload.podsList.items.forEach(pod => { 109 | if (!pod.metadata.ownerReferences) { 110 | orphanPods.push(pod); 111 | } 112 | }); 113 | 114 | this._state.orphanPodsList = orphanPods; 115 | this.emit(WorkloadsEvents.WorkloadPodsFetchedEvent, this); 116 | 117 | if (this._state.orphanPodsList && this._state.orphanPodsList.length > 0) { 118 | this.emit(WorkloadsEvents.WorkloadsFoundEvent, this); 119 | } 120 | } 121 | 122 | private _state: IWorkloadsStoreState; 123 | private _workloadActions: WorkloadsActions; 124 | private _podsActions: PodsActions; 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Contracts/Contracts"; 2 | export * from "./Contracts/KubeServiceBase"; 3 | export * from "./WebUI/Common/KubeSummary"; -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "charset": "utf8", 4 | "declaration": true, 5 | "experimentalDecorators": true, 6 | "jsx": "react", 7 | "lib": [ 8 | "es5", 9 | "es6", 10 | "dom", 11 | "es2015.promise" 12 | ], 13 | "module": "amd", 14 | "target": "es5", 15 | "moduleResolution": "node", 16 | "noImplicitAny": false, 17 | "noImplicitThis": true, 18 | "skipDefaultLibCheck": true, 19 | "strictPropertyInitialization": false, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "outDir": "../dist", 23 | "rootDir": "./", 24 | "baseUrl": "./", 25 | "types": [ 26 | "react" 27 | ], 28 | "paths": { 29 | "@azurepipelines/azdevops-kube-summary/dist/*": [ 30 | "./*" 31 | ] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Contracts/KubeServiceBase.test.ts: -------------------------------------------------------------------------------- 1 | import { KubeResourceType, KubeServiceBase } from "../../src/Contracts/KubeServiceBase"; 2 | import { MockKubeService } from "../WebUI/MockKubeService"; 3 | import { localeFormat, newGuid } from "azure-devops-ui/Core/Util/String"; 4 | 5 | describe("KubeServiceBase Tests", () => { 6 | let service: KubeServiceBase; 7 | beforeAll(() => { 8 | service = new MockKubeService(); 9 | }); 10 | 11 | afterAll(() => { service = null; }); 12 | 13 | it("getPods calls with right input", () => { 14 | expect.assertions(1); 15 | return service.getPods().then(output => { 16 | expect(output).toBe(KubeResourceType.Pods); 17 | }); 18 | }); 19 | 20 | it("getDeployments calls with right input", () => { 21 | expect.assertions(1); 22 | return service.getDeployments().then(output => { 23 | expect(output).toBe(KubeResourceType.Deployments); 24 | }); 25 | }); 26 | 27 | it("getReplicaSets calls with right input", () => { 28 | expect.assertions(1); 29 | return service.getReplicaSets().then(output => { 30 | expect(output).toBe(KubeResourceType.ReplicaSets); 31 | }); 32 | }); 33 | 34 | it("getServices calls with right input", () => { 35 | expect.assertions(1); 36 | return service.getServices().then(output => { 37 | expect(output).toBe(KubeResourceType.Services); 38 | }); 39 | }); 40 | 41 | it("getDaemonSets calls with right input", () => { 42 | expect.assertions(1); 43 | return service.getDaemonSets().then(output => { 44 | expect(output).toBe(KubeResourceType.DaemonSets); 45 | }); 46 | }); 47 | 48 | it("getStatefulSets calls with right input", () => { 49 | expect.assertions(1); 50 | return service.getStatefulSets().then(output => { 51 | expect(output).toBe(KubeResourceType.StatefulSets); 52 | }); 53 | }); 54 | 55 | it("getPods calls with labelSelector as input", () => { 56 | expect.assertions(1); 57 | const labelSelector: string = "app=app"; 58 | return service.getPods(labelSelector).then(output => { 59 | expect(output).toBe(localeFormat("{0}{1}", KubeResourceType.Pods, labelSelector)); 60 | }); 61 | }); 62 | 63 | it("getPodLog should give log", () => { 64 | expect.assertions(1); 65 | const podName: string = newGuid(); 66 | return service.getPodLog(podName).then(output => { 67 | expect(output).toBe(localeFormat("Output:{0}", podName)); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/TestCore.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const Enzyme = require("enzyme"); 3 | const EnzymeAdapterReact16 = require("enzyme-adapter-react-16"); 4 | const EnzymeToJson = require("enzyme-to-json"); 5 | 6 | Enzyme.configure({ 7 | adapter: new EnzymeAdapterReact16() 8 | }); 9 | 10 | exports.shallow = function shallow(nextElement, options) { 11 | const wrapper = Enzyme.shallow(nextElement, { 12 | ...options 13 | }); 14 | wrapper.toJson = function () { 15 | return EnzymeToJson.shallowToJson(this); 16 | }; 17 | 18 | return wrapper; 19 | }; 20 | 21 | exports.mount = function mount(nextElement, options) { 22 | const wrapper = Enzyme.mount(nextElement, { 23 | ...options 24 | }); 25 | wrapper.toJson = function () { 26 | return EnzymeToJson.mountToJson(this); 27 | }; 28 | 29 | return wrapper; 30 | }; 31 | 32 | exports.render = function render(nextElement, options) { 33 | const wrapper = Enzyme.render(nextElement, { 34 | ...options 35 | }); 36 | wrapper.toJson = function () { 37 | return EnzymeToJson.renderToJson(this); 38 | }; 39 | 40 | return wrapper; 41 | }; 42 | 43 | var spyConsole; 44 | // beforeAll/afterAll will always fail last scenario [if there are any errors]. 45 | // beforeEach will help to find the scenario causing the issue. 46 | beforeEach(() => { 47 | // react warnings are logged as console.error 48 | // so spying on error function of console. 49 | spyConsole = jest.spyOn(console, "error"); 50 | }); 51 | 52 | afterEach(() => { 53 | if (spyConsole) { 54 | expect(spyConsole).toHaveBeenCalledTimes(0); 55 | spyConsole.mockRestore(); 56 | } 57 | 58 | spyConsole = null; 59 | }); 60 | 61 | exports.spyConsole = spyConsole; -------------------------------------------------------------------------------- /tests/WebUI/ActionsCreatorTests/ImageDetailsActionsCreator.test.ts: -------------------------------------------------------------------------------- 1 | import { ImageDetailsActionsCreator } from "../../../src/WebUI/ImageDetails/ImageDetailsActionsCreator"; 2 | import { ImageDetailsStore } from "../../../src/WebUI/ImageDetails/ImageDetailsStore"; 3 | import { StoreManager } from "../../../src/WebUI/FluxCommon/StoreManager"; 4 | import { ActionsCreatorManager } from "../../../src/WebUI/FluxCommon/ActionsCreatorManager"; 5 | import { IImageDetails } from "../../../src/Contracts/Types"; 6 | import { KubeFactory } from "../../../src/WebUI/KubeFactory"; 7 | import { MockImageService } from "../MockImageService"; 8 | 9 | describe("ImageDetailsActionsCreator getImageDetails Tests", () => { 10 | const mockImageDetails: IImageDetails = { 11 | imageName: "https://k8s.gcr.io/coredns@sha2563e2be1cec87aca0b74b7668bbe8c02964a95a402e45ceb51b2252629d608d03a", 12 | imageUri: "https://k8s.gcr.io/coredns@sha2563e2be1cec87aca0b74b7668bbe8c02964a95a402e45ceb51b2252629d608d03a", 13 | baseImageName: "k8s.gcr.io/coredns23", 14 | imageType: "", 15 | mediaType: "", 16 | tags: ["test-image-tag"], 17 | layerInfo: [{ "directive": "ADD", "arguments": "mock argument", "createdOn": new Date("2019-06-25T05:50:11+05:30"), "size": "88.9MB" }], 18 | runId: 1, 19 | pipelineVersion: "20", 20 | pipelineName: "test-image-pipelineName", 21 | pipelineId: "111", 22 | jobName: "test-image-jobName", 23 | imageSize: "1000MB", 24 | }; 25 | 26 | let mockImageService = new MockImageService(); 27 | KubeFactory.getImageService = jest.fn().mockReturnValue(mockImageService); 28 | 29 | const mockShowImageDetailsAction = jest.fn().mockImplementation((imageDetails: IImageDetails): void => { }); 30 | 31 | let imageDetailsStore = StoreManager.GetStore(ImageDetailsStore); 32 | 33 | beforeEach(() => { 34 | // Mock return value for image service getImageDetails 35 | mockImageService.getImageDetails = jest.fn().mockReturnValue(new Promise(() => mockImageDetails)); 36 | }); 37 | 38 | afterEach(() => { 39 | jest.clearAllMocks(); 40 | }); 41 | 42 | it("If imageDetails are not defined in store then call image service", () => { 43 | // mock imageDetailsStore.getImageDetails 44 | imageDetailsStore.getImageDetails = jest.fn().mockReturnValueOnce(undefined); 45 | 46 | ActionsCreatorManager.GetActionCreator(ImageDetailsActionsCreator).getImageDetails("test-image", mockShowImageDetailsAction); 47 | expect(mockImageService.getImageDetails).toHaveBeenCalledWith("test-image"); 48 | expect(mockShowImageDetailsAction).not.toBeCalled(); // Not to be called as image details mock value is undefined here 49 | }); 50 | 51 | it("If image details are present in store then do not call image service", () => { 52 | // mock imageDetailsStore.getImageDetails 53 | imageDetailsStore.getImageDetails = jest.fn().mockReturnValueOnce(mockImageDetails); 54 | 55 | ActionsCreatorManager.GetActionCreator(ImageDetailsActionsCreator).getImageDetails("test-image", mockShowImageDetailsAction); 56 | expect(mockImageService.getImageDetails).not.toBeCalled(); 57 | expect(mockShowImageDetailsAction).toBeCalledWith(mockImageDetails); 58 | }); 59 | }); -------------------------------------------------------------------------------- /tests/WebUI/Components/KubeCardWithTable.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { ITableComponentProperties, KubeCardWithTable } from "../../../src/WebUI/Common/KubeCardWithTable"; 4 | import { shallow } from "../../TestCore"; 5 | 6 | describe("KubeCardWithTable component tests", () => { 7 | it("Check header of the KubeCardWithTable component", () => { 8 | const props: ITableComponentProperties = { 9 | headingText: "Heading", 10 | columns: [], 11 | items: [], 12 | onRenderItemColumn: () => null 13 | }; 14 | 15 | const wrapper = shallow(); 16 | const heading = wrapper.find("HeaderTitle"); 17 | expect(heading && heading.length > 0).toBeTruthy(); 18 | }); 19 | }); -------------------------------------------------------------------------------- /tests/WebUI/Components/ServiceDetails.test.tsx: -------------------------------------------------------------------------------- 1 | import { V1Service } from "@kubernetes/client-node"; 2 | import * as String_Utils from "azure-devops-ui/Core/Util/String"; 3 | import { Statuses } from "azure-devops-ui/Status"; 4 | import * as React from "react"; 5 | import { ServiceDetails } from "../../../src/WebUI/Services/ServiceDetails"; 6 | import { IServiceItem } from "../../../src/WebUI/Types"; 7 | import { mount, shallow } from "../../TestCore"; 8 | 9 | describe("ServiceDetails component tests", () => { 10 | 11 | const name = "some package name" + String_Utils.newGuid(); 12 | const uid = "f32f9f29-ebed-11e8-ac56-829606b05f65"; 13 | const createdDate = new Date(2015, 10, 10); 14 | const serviceObj = { 15 | "apiVersion": "v1", 16 | "kind": "Service", 17 | "metadata": { 18 | "annotations": { 19 | "kubectl.kubernetes.io/last-applied-configuration": "some annotation" 20 | }, 21 | "creationTimestamp": createdDate, 22 | "name": name, 23 | "namespace": "some-namespace", 24 | "uid": uid 25 | }, 26 | "spec": { 27 | "clusterIP": "10.0.93.21", 28 | "externalTrafficPolicy": "Cluster", 29 | "ports": [ 30 | { 31 | "nodePort": 30474, 32 | "port": 80, 33 | "protocol": "TCP", 34 | "targetPort": 80 35 | } 36 | ], 37 | "selector": { 38 | "app": "test-app-one" 39 | }, 40 | "sessionAffinity": "None", 41 | "type": "LoadBalancer" 42 | }, 43 | "status": { 44 | "loadBalancer": { 45 | "ingress": [ 46 | { 47 | "ip": "52.163.187.200" 48 | } 49 | ] 50 | } 51 | } 52 | } as any; 53 | 54 | const item = { 55 | package: name, 56 | type: "type", 57 | clusterIP: "clusterIp", 58 | externalIP: "externalIp", 59 | port: "port", 60 | uid: uid, 61 | creationTimestamp: createdDate, 62 | service: serviceObj as V1Service 63 | } as IServiceItem; 64 | 65 | it("Check header of the ServiceDetails component", () => { 66 | const wrapper = shallow(); 67 | const pageClass = ".service-details-page"; 68 | 69 | const pageContainer = wrapper.find(pageClass); 70 | expect(pageContainer && pageContainer.length > 0).toBeTruthy(); 71 | 72 | const heading = wrapper.find("PageTopHeader"); 73 | expect(heading && heading.length > 0).toBeTruthy(); 74 | expect(heading.prop("title")).toStrictEqual(item.package); 75 | expect(heading.prop("className")).toStrictEqual("s-details-header"); 76 | const statusProps = item.type === "LoadBalancer" && !item.externalIP ? Statuses.Running : Statuses.Success; 77 | expect(heading.prop("statusProps")).toStrictEqual(statusProps); 78 | }); 79 | 80 | it("Check service ServiceDetails component after mount", () => { 81 | const wrapper = mount(); 82 | 83 | const pageContent = wrapper.find(".service-details-page-content"); 84 | expect(pageContent && pageContent.length > 0).toBeTruthy(); 85 | 86 | const sTableKeys = wrapper.find(".service-details-card"); 87 | expect(sTableKeys && sTableKeys.length > 0).toBeTruthy(); 88 | 89 | const sDetails = wrapper.find(".service-full-details-table"); 90 | expect(sDetails && sDetails.length > 0).toBeTruthy(); 91 | }); 92 | }); -------------------------------------------------------------------------------- /tests/WebUI/Components/WorkloadDetails.test.tsx: -------------------------------------------------------------------------------- 1 | import { V1Service, V1ReplicaSet, V1ReplicaSetSpec, V1ReplicaSetStatus, V1ObjectMeta } from "@kubernetes/client-node"; 2 | import * as String_Utils from "azure-devops-ui/Core/Util/String"; 3 | import { Statuses } from "azure-devops-ui/Status"; 4 | import * as React from "react"; 5 | import { WorkloadDetails } from "../../../src/WebUI/Workloads/WorkloadDetails"; 6 | import { IServiceItem } from "../../../src/WebUI/Types"; 7 | import { mount, shallow } from "../../TestCore"; 8 | import { SelectedItemKeys } from "../../../src/WebUI/Constants"; 9 | 10 | describe("WorkloadDetails component tests", () => { 11 | 12 | const name = "some package name" + String_Utils.newGuid(); 13 | const uid = "3014b92b-724e-49a8-8fd3-135d025de247"; 14 | const createdDate = new Date(2015, 10, 10); 15 | 16 | const item = { 17 | "metadata": { 18 | "name": "azure-vote-front-f747b5d4b", 19 | "namespace": "default", 20 | "uid": "b5187188-495e-11e9-96d5-96c741883af0", 21 | "creationTimestamp": "2019-03-18T09:17:52Z", 22 | "labels": { "app": "azure-vote-front", "pod-template-hash": "f747b5d4b" }, 23 | "annotations": { "azure-pipelines/execution": "executionname" }, 24 | "ownerReferences": [{ 25 | "apiVersion": "apps/v1", 26 | "kind": "Deployment", 27 | "name": "azure-vote-front", 28 | "uid": "b512b229-495e-11e9-96d5-96c741883af0", 29 | }] 30 | } as any, 31 | "spec": { 32 | "replicas": 6, 33 | "minReadySeconds": 5, 34 | "selector": { "matchLabels": { "app": "azure-vote-front", "pod-template-hash": "f747b5d4b" } }, 35 | "template": { 36 | "metadata": { 37 | "labels": { "app": "azure-vote-front", "pod-template-hash": "f747b5d4b" } 38 | }, 39 | "spec": { 40 | "containers": [{ 41 | "name": "azure-vote-front", 42 | "image": "microsoft/azure-vote-front:v1", 43 | "ports": [{ "containerPort": 80, "protocol": "TCP" }], 44 | "env": [{ "name": "REDIS", "value": "azure-vote-back" }], 45 | "resources": { "limits": { "cpu": "500m" }, "requests": { "cpu": "250m" } }, 46 | "terminationMessagePath": "/dev/termination-log", 47 | "terminationMessagePolicy": "File", 48 | "imagePullPolicy": "IfNotPresent" 49 | }], 50 | "restartPolicy": "Always", 51 | "terminationGracePeriodSeconds": 30, 52 | "dnsPolicy": "ClusterFirst", 53 | "securityContext": {}, 54 | "schedulerName": "default-scheduler" 55 | } 56 | } 57 | } as any, 58 | "status": { "replicas": 6, "fullyLabeledReplicas": 6, "readyReplicas": 6, "availableReplicas": 6, "observedGeneration": 3 } as any 59 | } as V1ReplicaSet; 60 | 61 | it("Check header of the WorkloadDetails component", () => { 62 | const wrapper = shallow( { 67 | return { statusProps: Statuses.Success, podsTooltip: "", pods: "" }; 68 | }} 69 | />); 70 | const pageClass = ".workload-details-page"; 71 | 72 | const pageContainer = wrapper.find(pageClass); 73 | expect(pageContainer && pageContainer.length > 0).toBeTruthy(); 74 | 75 | const heading = wrapper.find("PageTopHeader"); 76 | expect(heading && heading.length > 0).toBeTruthy(); 77 | expect(heading.prop("title")).toStrictEqual(item.metadata.name); 78 | expect(heading.prop("className")).toStrictEqual("wl-header"); 79 | }); 80 | 81 | it("Check WorkloadDetails component after mount", () => { 82 | const wrapper = mount( { 87 | return { statusProps: Statuses.Failed, podsTooltip: "", pods: "" }; 88 | }} 89 | />); 90 | 91 | const pageContent = wrapper.find(".workload-details-page-content"); 92 | expect(pageContent && pageContent.length > 0).toBeTruthy(); 93 | 94 | const detailsCard = wrapper.find(".workload-details-card"); 95 | expect(detailsCard && detailsCard.length > 0).toBeTruthy(); 96 | 97 | const wlDetails = wrapper.find(".workload-full-details-table"); 98 | expect(wlDetails && wlDetails.length > 0).toBeTruthy(); 99 | }); 100 | }); -------------------------------------------------------------------------------- /tests/WebUI/MockImageService.ts: -------------------------------------------------------------------------------- 1 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 2 | import { KubeResourceType, KubeServiceBase } from "../../src/Contracts/KubeServiceBase"; 3 | import { IImageService } from "../../src/Contracts/Contracts"; 4 | import { IImageDetails } from "../../src/Contracts/Types"; 5 | 6 | export class MockImageService implements IImageService { 7 | public hasImageDetails(listImages: Array): Promise { 8 | return Promise.resolve({}); 9 | } 10 | 11 | public getImageDetails(imageName: string): Promise { 12 | return Promise.resolve({} as IImageDetails); 13 | } 14 | 15 | public getImageProvenances(imageNames: string[]): Promise { 16 | return Promise.resolve({}); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/WebUI/MockKubeService.ts: -------------------------------------------------------------------------------- 1 | import { localeFormat } from "azure-devops-ui/Core/Util/String"; 2 | import { KubeResourceType, KubeServiceBase } from "../../src/Contracts/KubeServiceBase"; 3 | 4 | export class MockKubeService extends KubeServiceBase { 5 | public fetch(resourceType: KubeResourceType, labelSelector?: string): Promise { 6 | return Promise.resolve(labelSelector ? localeFormat("{0}{1}", resourceType, labelSelector || "") : resourceType); 7 | } 8 | 9 | public getPodLog(podName: string): Promise { 10 | return Promise.resolve(localeFormat("Output:{0}", podName)); 11 | } 12 | } -------------------------------------------------------------------------------- /tests/WebUI/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../../src/WebUI/Utils"; 2 | 3 | describe("Utils isOwnerMatched Tests", () => { 4 | const isOwnerMatchedData = [ 5 | [ 6 | "ownerMatched", 7 | { ownerReferences: [{ uid: "f310d74e-ebed-11e8-ac56-829606b05f65" }] }, 8 | "f310d74e-ebed-11e8-ac56-829606b05f65", 9 | true 10 | ], 11 | [ 12 | "ownerNotMatched", 13 | { ownerReferences: [{ uid: "f310d74e-ebed-11e8-ac56-829606b05f65" }] }, 14 | "f310d74e-ebfd-11e8-ac56-829606b05f65", 15 | false 16 | ], 17 | [ 18 | "zeroOwners", 19 | { ownerReferences: [] }, 20 | "f310d74e-ebfd-11e8-ac56-829606b05f65", 21 | false 22 | ], 23 | [ 24 | "moreOwnersFirstMatched", 25 | { 26 | ownerReferences: [ 27 | { uid: "f310d74e-ebfd-11e8-ac56-829606b05f65" }, 28 | { uid: "f310d74e-ebed-11e8-ac56-829606b05f65" } 29 | ] 30 | }, 31 | "f310d74e-ebfd-11e8-ac56-829606b05f65", 32 | true 33 | ], 34 | [ 35 | "moreOwnersFirstNotMatched", 36 | { 37 | ownerReferences: [ 38 | { uid: "f310d74e-ebed-11e8-ac56-829606b05f65" }, 39 | { uid: "f310d74e-ebfd-11e8-ac56-829606b05f65" } 40 | ] 41 | }, 42 | "f310d74e-ebfd-11e8-ac56-829606b05f65", 43 | true 44 | ], 45 | ]; 46 | 47 | it.each(isOwnerMatchedData)("isOwnerMatched checking for:: %s", (testName, metadataObj, toCheckOwner, expectedResult) => { 48 | expect(Utils.isOwnerMatched(metadataObj, toCheckOwner)).toStrictEqual(expectedResult); 49 | }); 50 | }); 51 | 52 | describe("Utils getPillTags count Tests", () => { 53 | const getPillTagsData = [ 54 | [ 55 | "zeroLabels", 56 | {}, 57 | 0 58 | ], 59 | [ 60 | "nullInput", 61 | null, 62 | 0 63 | ], 64 | [ 65 | "oneLabel", 66 | { "one": "oneLabel" }, 67 | 1 68 | ], 69 | [ 70 | "twoLabels", 71 | { 72 | "second": "secondLabel", 73 | "one": "oneLabel" 74 | }, 75 | 2 76 | ] 77 | ]; 78 | 79 | it.each(getPillTagsData)("getPillTags checking for:: %s", (testName, items, labelCount) => { 80 | expect(Utils.getPillTags(items).length).toStrictEqual(labelCount); 81 | }); 82 | }); 83 | 84 | describe("Utils getPillTags value check Tests", () => { 85 | const getPillTagsData = [ 86 | [ 87 | "oneLabel value", 88 | { "one": "oneLabel" }, 89 | ["one=oneLabel"] 90 | ], 91 | [ 92 | "twoLabels value", 93 | { 94 | "second": "secondLabel", 95 | "one": "oneLabel" 96 | }, 97 | ["second=secondLabel", "one=oneLabel"] 98 | ], 99 | [ 100 | "space with quotes value", 101 | { 102 | "second": "secondLabel\" ", 103 | "one": " oneLabel", 104 | "three": " '\"three \" Label'\" ", 105 | "four": "\"fourLable\"", 106 | "five": "'fiveLable'", 107 | "six": "\"'sixLabel\"'" 108 | }, 109 | ["second=secondLabel", "one=oneLabel", "three=three \" Label", "four=fourLable", "five=fiveLable", "six=sixLabel"] 110 | ] 111 | ]; 112 | 113 | it.each(getPillTagsData)("getPillTags checking for:: %s", (testName, items, output) => { 114 | Utils.getPillTags(items).forEach((item, index) => { expect(item).toStrictEqual(output[index]); }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "charset": "utf8", 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "outDir": "../dist_tests", 10 | "jsx": "react" 11 | }, 12 | "typeRoots": [ 13 | "@types/", 14 | ], 15 | "types": [ 16 | "jest", 17 | "node", 18 | "enzyme" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } -------------------------------------------------------------------------------- /tests/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const globSync = require("glob").sync 4 | 5 | const searchPattern = "/**/*"; 6 | const outputPathRelativePath = "./../dist_tests"; 7 | const testsFolderName = "tests"; 8 | const outputPath = path.resolve(__dirname, outputPathRelativePath); 9 | 10 | // Webpack entry points. Mapping from resulting bundle name to the source file entry. 11 | const entries = {}; 12 | // Loop through subfolders for .test.ts/tsx/js files 13 | const allFiles = globSync(__dirname + searchPattern); 14 | allFiles.forEach(f => { 15 | if (fs.statSync(f).isFile()) { 16 | if (f.endsWith(".test.ts") || f.endsWith(".test.tsx") || f.endsWith(".test.js")) { 17 | // find test file, use the relativepath + name as the key for webpack 18 | const relativePath = path.relative(process.cwd(), f); 19 | const parsedPath = path.parse(path.normalize(relativePath)); 20 | const fName = path.join(parsedPath.dir, parsedPath.name); 21 | entries[fName] = path.normalize(f); 22 | } 23 | } 24 | }); 25 | 26 | module.exports = { 27 | entry: entries, 28 | output: { 29 | filename: "[name].js", 30 | path: outputPath 31 | }, 32 | resolve: { 33 | extensions: [".ts", ".tsx", ".js"], 34 | }, 35 | stats: { 36 | warnings: false 37 | }, 38 | module: { 39 | rules: [{ 40 | test: /\.tsx?$/, 41 | loader: "ts-loader" 42 | }, 43 | { 44 | test: /\.scss$/, 45 | use: ["style-loader", "css-loader", "azure-devops-ui/buildScripts/css-variables-loader", "sass-loader"] 46 | }, 47 | { 48 | test: /\.css$/, 49 | use: ["style-loader", "css-loader"], 50 | }, 51 | { 52 | test: /\.woff$/, 53 | use: [{ 54 | loader: 'base64-inline-loader' 55 | }] 56 | }, 57 | { 58 | test: /\.html$/, 59 | loader: "file-loader" 60 | }, 61 | { test: /\.(png|jpg|svg)$/, loader: 'file-loader' }, 62 | ] 63 | }, 64 | node: { 65 | fs: 'empty', 66 | tls: 'mock', 67 | child_process: 'empty', 68 | net: 'empty' 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /webapp.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CircularDependencyPlugin = require("circular-dependency-plugin"); 4 | 5 | module.exports = { 6 | entry: { 7 | "azdevops-kube-summary": "./src/index.ts", 8 | "azdevops-kube-summary.min": "./src/index.ts" 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, "_bin/webAppPackage/_bundles"), 12 | filename: "[name].js", 13 | libraryTarget: "umd", 14 | library: "webapp-kube-summary", 15 | umdNamedDefine: true 16 | }, 17 | resolve: { 18 | extensions: [".ts", ".tsx", ".js"] 19 | }, 20 | devtool: "source-map", 21 | optimization: { 22 | minimizer: [ 23 | new TerserPlugin({ 24 | sourceMap: true, 25 | include: /\.min\.js$/, 26 | parallel: 4 27 | }) 28 | ] 29 | }, 30 | plugins: [ 31 | new CircularDependencyPlugin({ 32 | onStart({ compilation }) { 33 | // `onStart` is called before the cycle detection starts 34 | console.log("Detecting webpack modules cycles -- start."); 35 | }, 36 | onDetected({ module: webpackModuleRecord, paths, compilation }) { 37 | // `paths` will be an Array of the relative module paths that make up the cycle 38 | // `module` will be the module record generated by webpack that caused the cycle 39 | const cyclePaths = paths.join(' -> '); 40 | compilation.errors.push(new Error(cyclePaths)) 41 | console.error("Cycle detected: " + cyclePaths); 42 | }, 43 | // `onEnd` is called before the cycle detection ends 44 | onEnd({ compilation }) { 45 | console.log("Done detecting webpack modules cycles."); 46 | }, 47 | failOnError: true, 48 | cwd: process.cwd() 49 | }) 50 | ], 51 | module: { 52 | rules: [ 53 | { 54 | test: /\.tsx?$/, 55 | loader: "ts-loader" 56 | }, 57 | { 58 | test: /\.scss$/, 59 | use: ["style-loader", "css-loader", "./buildScripts/css-variables-loader", "sass-loader"] 60 | }, 61 | { 62 | test: /\.css$/, 63 | use: ["style-loader", "css-loader"], 64 | }, 65 | { 66 | test: /\.woff$/, 67 | use: [{ 68 | loader: "base64-inline-loader" 69 | }] 70 | }, 71 | { 72 | test: /\.html$/, 73 | loader: "file-loader" 74 | }, 75 | { test: /\.(png|jpg|svg)$/, loader: "file-loader" }, 76 | ] 77 | }, 78 | node: { 79 | fs: "empty", 80 | tls: "mock", 81 | child_process: "empty", 82 | net: "empty" 83 | }, 84 | externals: { 85 | "react": "react", 86 | "react-dom": "react-dom" 87 | } 88 | } --------------------------------------------------------------------------------