ImageSelector.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import React, { Component } from 'react';
  2. import styled from 'styled-components';
  3. import info from '../../assets/info.svg';
  4. import edit from '../../assets/edit.svg';
  5. import api from '../../shared/api';
  6. import { integrationList } from '../../shared/common';
  7. import { Context } from '../../shared/Context';
  8. import { ImageType } from '../../shared/types';
  9. import Loading from '../Loading';
  10. import TagList from './TagList';
  11. type PropsType = {
  12. forceExpanded?: boolean,
  13. selectedImageUrl: string | null,
  14. selectedTag: string | null,
  15. setSelectedImageUrl: (x: string) => void,
  16. setSelectedTag: (x: string) => void,
  17. setCurrentView: (x: string) => void,
  18. };
  19. type StateType = {
  20. isExpanded: boolean,
  21. loading: boolean,
  22. error: boolean,
  23. images: ImageType[],
  24. clickedImage: ImageType | null,
  25. registryId: number | null, // For passing registry ID to tag list
  26. };
  27. export default class ImageSelector extends Component<PropsType, StateType> {
  28. state = {
  29. isExpanded: this.props.forceExpanded,
  30. loading: true,
  31. error: false,
  32. images: [] as ImageType[],
  33. clickedImage: null as ImageType | null,
  34. registryId: null as number | null,
  35. }
  36. componentDidMount() {
  37. const { currentProject, setCurrentError } = this.context;
  38. let images = [] as ImageType[]
  39. api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
  40. if (err) {
  41. console.log(err);
  42. } else {
  43. let registries = res.data;
  44. registries.forEach(async (registry: any, i: number) => {
  45. await new Promise((nextController: (res?: any) => void) => {
  46. api.getImageRepos('<token>', {},
  47. {
  48. project_id: currentProject.id,
  49. registry_id: registry.id,
  50. }, (err: any, res: any) => {
  51. if (err && this.state.loading) {
  52. this.setState({ error: true, loading: false });
  53. } else {
  54. let newImg = res.data.map((img: any) => {
  55. return {
  56. kind: registry.service,
  57. source: img.name
  58. }
  59. })
  60. images.push(...newImg)
  61. this.setState({
  62. images,
  63. registryId: registry.id,
  64. loading: false,
  65. error: false,
  66. });
  67. nextController()
  68. }
  69. });
  70. })
  71. });
  72. }
  73. });
  74. }
  75. renderImageList = () => {
  76. let { images, loading, error } = this.state;
  77. if (loading) {
  78. return <LoadingWrapper><Loading /></LoadingWrapper>
  79. } else if (error || !images) {
  80. return <LoadingWrapper>Error loading repos</LoadingWrapper>
  81. } else if (images.length === 0) {
  82. return (
  83. <LoadingWrapper>
  84. No registries found.
  85. <Highlight onClick={() => this.props.setCurrentView('integrations')}>
  86. Link your registry.
  87. </Highlight>
  88. </LoadingWrapper>
  89. );
  90. }
  91. return images.map((image: ImageType, i: number) => {
  92. let icon = integrationList[image.kind] && integrationList[image.kind].icon;
  93. if (!icon) {
  94. icon = integrationList['docker'].icon;
  95. }
  96. return (
  97. <ImageItem
  98. key={i}
  99. isSelected={image.source === this.props.selectedImageUrl}
  100. lastItem={i === images.length - 1}
  101. onClick={() => {
  102. this.props.setSelectedImageUrl(image.source);
  103. this.setState({ clickedImage: image });
  104. }}
  105. >
  106. <img src={icon && icon} />{image.source}
  107. </ImageItem>
  108. );
  109. });
  110. }
  111. renderBackButton = () => {
  112. let { setSelectedImageUrl } = this.props;
  113. if (this.state.clickedImage) {
  114. return (
  115. <BackButton
  116. width='175px'
  117. onClick={() => {
  118. setSelectedImageUrl('');
  119. this.setState({ clickedImage: null });
  120. }}
  121. >
  122. <i className="material-icons">keyboard_backspace</i>
  123. Select Image Repo
  124. </BackButton>
  125. );
  126. }
  127. }
  128. renderExpanded = () => {
  129. let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
  130. if (!this.state.clickedImage) {
  131. return (
  132. <div>
  133. <ExpandedWrapper>
  134. {this.renderImageList()}
  135. </ExpandedWrapper>
  136. {this.renderBackButton()}
  137. </div>
  138. );
  139. } else {
  140. return (
  141. <div>
  142. <ExpandedWrapper>
  143. <TagList
  144. selectedTag={selectedTag}
  145. selectedImageUrl={selectedImageUrl}
  146. setSelectedTag={setSelectedTag}
  147. registryId={this.state.registryId}
  148. />
  149. </ExpandedWrapper>
  150. {this.renderBackButton()}
  151. </div>
  152. );
  153. }
  154. }
  155. renderSelected = () => {
  156. let { selectedImageUrl, setSelectedImageUrl } = this.props;
  157. let { clickedImage } = this.state;
  158. let icon = info;
  159. if (clickedImage) {
  160. icon = clickedImage.kind;
  161. icon = integrationList[clickedImage.kind] && integrationList[clickedImage.kind].icon;
  162. if (!icon) {
  163. icon = integrationList['docker'].icon;
  164. }
  165. } else if (selectedImageUrl && selectedImageUrl !== '') {
  166. icon = edit;
  167. }
  168. return (
  169. <Label>
  170. <img src={icon} />
  171. <Input
  172. onClick={(e: any) => e.stopPropagation()}
  173. value={selectedImageUrl}
  174. onChange={(e: any) => {
  175. setSelectedImageUrl(e.target.value);
  176. this.setState({ clickedImage: null });
  177. }}
  178. placeholder='Enter or select your container image URL'
  179. />
  180. </Label>
  181. );
  182. }
  183. handleClick = () => {
  184. if (!this.props.forceExpanded) {
  185. this.setState({ isExpanded: !this.state.isExpanded });
  186. }
  187. }
  188. render() {
  189. return (
  190. <div>
  191. <StyledImageSelector
  192. onClick={this.handleClick}
  193. isExpanded={this.state.isExpanded}
  194. forceExpanded={this.props.forceExpanded}
  195. >
  196. {this.renderSelected()}
  197. {this.props.forceExpanded ? null : <i className="material-icons">{this.state.isExpanded ? 'close' : 'build'}</i>}
  198. </StyledImageSelector>
  199. {this.state.isExpanded ? this.renderExpanded() : null}
  200. </div>
  201. );
  202. }
  203. }
  204. ImageSelector.contextType = Context;
  205. const Highlight = styled.div`
  206. text-decoration: underline;
  207. margin-left: 10px;
  208. color: #949eff;
  209. cursor: pointer;
  210. padding: 3px 0;
  211. `;
  212. const BackButton = styled.div`
  213. display: flex;
  214. align-items: center;
  215. justify-content: space-between;
  216. margin-top: 10px;
  217. cursor: pointer;
  218. font-size: 13px;
  219. padding: 5px 13px;
  220. border: 1px solid #ffffff55;
  221. border-radius: 3px;
  222. width: ${(props: { width: string }) => props.width};
  223. color: white;
  224. background: #ffffff11;
  225. :hover {
  226. background: #ffffff22;
  227. }
  228. > i {
  229. color: white;
  230. font-size: 16px;
  231. margin-right: 6px;
  232. }
  233. `;
  234. const Input = styled.input`
  235. outline: 0;
  236. background: none;
  237. border: 0;
  238. font-size: 13px;
  239. width: calc(100% - 60px);
  240. color: white;
  241. `;
  242. const ImageItem = styled.div`
  243. display: flex;
  244. width: 100%;
  245. font-size: 13px;
  246. border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
  247. color: #ffffff;
  248. user-select: none;
  249. align-items: center;
  250. padding: 10px 0px;
  251. cursor: pointer;
  252. background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
  253. :hover {
  254. background: #ffffff22;
  255. > i {
  256. background: #ffffff22;
  257. }
  258. }
  259. > img {
  260. width: 18px;
  261. height: 18px;
  262. margin-left: 12px;
  263. margin-right: 12px;
  264. filter: grayscale(100%);
  265. }
  266. `;
  267. const LoadingWrapper = styled.div`
  268. padding: 30px 0px;
  269. background: #ffffff11;
  270. display: flex;
  271. align-items: center;
  272. font-size: 13px;
  273. justify-content: center;
  274. color: #ffffff44;
  275. `;
  276. const ExpandedWrapper = styled.div`
  277. margin-top: 10px;
  278. width: 100%;
  279. border-radius: 3px;
  280. border: 1px solid #ffffff44;
  281. max-height: 275px;
  282. overflow-y: auto;
  283. `;
  284. const Label = styled.div`
  285. display: flex;
  286. align-items: center;
  287. flex: 1;
  288. > img {
  289. width: 18px;
  290. height: 18px;
  291. margin-left: 12px;
  292. margin-right: 12px;
  293. }
  294. `;
  295. const StyledImageSelector = styled.div`
  296. width: 100%;
  297. border: 1px solid #ffffff55;
  298. background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
  299. border-radius: 3px;
  300. user-select: none;
  301. height: 40px;
  302. font-size: 13px;
  303. color: #ffffff;
  304. display: flex;
  305. align-items: center;
  306. justify-content: space-between;
  307. cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};
  308. :hover {
  309. background: #ffffff11;
  310. > i {
  311. background: #ffffff22;
  312. }
  313. }
  314. > i {
  315. font-size: 16px;
  316. color: #ffffff66;
  317. margin-right: 10px;
  318. display: flex;
  319. align-items: center;
  320. justify-content: center;
  321. border-radius: 20px;
  322. padding: 4px;
  323. }
  324. `;