Skip to content

Commit

Permalink
Merge pull request #178 from devtron-labs/tag-label
Browse files Browse the repository at this point in the history
App Tag label
  • Loading branch information
nishant-d authored Aug 24, 2021
2 parents aee476d + 7ba6844 commit e0f152c
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 48 deletions.
37 changes: 37 additions & 0 deletions src/components/app/appLabelCommon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PATTERNS } from '../../config';
import React, { lazy, Suspense, useCallback, useRef, useEffect, useState } from 'react';

export function validateTags(tag) {
var re = PATTERNS.APP_LABEL_CHIP;
let regExp = new RegExp(re);
let result = regExp.test(String(tag));
return result;
}

export const TAG_VALIDATION_MESSAGE = {
error: 'Please provide tags in key:value format only'
}

export const createOption = (label: string) => (
{
label: label,
value: label,
});

export function handleKeyDown(labelTags, setAppTagLabel, event) {

labelTags.inputTagValue = labelTags.inputTagValue.trim();
switch (event.key) {
case 'Enter':
case 'Tab':
case ',':
case ' ': // space
if (labelTags.inputTagValue) {
setAppTagLabel()
}
if (event.key !== 'Tab') {
event.preventDefault();
}
break;
}
}
113 changes: 106 additions & 7 deletions src/components/app/create/CreateApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { getTeamListMin, getAppListMin } from '../../../services/service';
import { createApp } from './service';
import { toast } from 'react-toastify';
import { ServerErrors } from '../../../modals/commonTypes';
import './createApp.css';
import { TAG_VALIDATION_MESSAGE, validateTags, createOption, handleKeyDown } from '../appLabelCommon'
import TagLabelSelect from '../details/TagLabelSelect';
import { ReactComponent as Error } from '../../../assets/icons/ic-warning.svg';
import { ReactComponent as Info } from '../../../assets/icons/ic-info-filled.svg';
import './createApp.css';

export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
rules = new ValidationRules();
_inputAppName: HTMLInputElement;

constructor(props) {
super(props);
this.state = {
Expand All @@ -30,6 +31,11 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
appName: "",
cloneId: 0,
},
labels: {
tags: [],
inputTagValue: '',
tagError: ''
},
isValid: {
projectId: false,
appName: false,
Expand All @@ -54,6 +60,40 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
}
}

handleInputChange = (inputTagValue) => {
let { form, isValid } = { ...this.state };
this.setState({
form, isValid,
labels: {
...this.state.labels,
inputTagValue: inputTagValue,
tagError: ''
},
})
}

handleTagsChange = (newValue: any, actionMeta: any) => {
this.setState({
labels: {
...this.state.labels,
tags: newValue || [],
tagError: ''
}
})
};

handleCreatableBlur = (e) => {
this.state.labels.inputTagValue = this.state.labels?.inputTagValue.trim()
if (!this.state.labels.inputTagValue) return
this.setState({
labels: {
inputTagValue: '',
tags: [...this.state.labels.tags, createOption(e.target.value)],
tagError: '',
}
});
};

handleAppname(event: React.ChangeEvent<HTMLInputElement>): void {
let { form, isValid } = { ...this.state };
form.appName = event.target.value;
Expand All @@ -67,20 +107,48 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
isValid.projectId = !!item;
this.setState({ form, isValid });
}
validateForm = (): boolean => {
if (this.state.labels.tags.length !== this.state.labels.tags.map(tag => tag.value).filter(tag => validateTags(tag)).length) {
this.setState({
labels: {
...this.state.labels,
tagError: TAG_VALIDATION_MESSAGE.error
}
})
return false
}
return true
}

createApp(): void {
const validForm = this.validateForm()
if (!validForm) {
return
}
this.setState({ showErrors: true });
let allKeys = Object.keys(this.state.isValid);
let isFormValid = allKeys.reduce((valid, key) => {
valid = valid && this.state.isValid[key];
valid = valid && this.state.isValid[key] && validForm;
return valid;
}, true);
if (!isFormValid) return;

let _optionTypes = [];
if (this.state.labels.tags && this.state.labels.tags.length > 0) {
this.state.labels.tags.forEach((_label) => {
let _splittedTag = _label.value.split(':');
_optionTypes.push({
key: _splittedTag[0],
value: _splittedTag[1]
})
})
}

let request = {
appName: this.state.form.appName,
teamId: this.state.form.projectId,
templateId: this.state.form.cloneId,
labels: _optionTypes
}
this.setState({ disableForm: true });
createApp(request).then((response) => {
Expand All @@ -90,7 +158,14 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
form.appName = response.result.appName;
isValid.appName = true;
isValid.projectId = true;
this.setState({ code: response.code, form, isValid, disableForm: false, showErrors: false }, () => {
this.setState({
code: response.code, form, isValid, disableForm: false, showErrors: false,
labels: {
...this.state.labels,
tags: response.result?.labels?.tags
}

}, () => {
toast.success('Your application is created. Go ahead and set it up.');
this.redirectToArtifacts(this.state.form.appId);
})
Expand All @@ -116,8 +191,20 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
this.props.history.push(url);
}

setAppTagLabel = () => {
let newTag = this.state.labels.inputTagValue.split(',').map((e) => { e = e.trim(); return createOption(e) });

this.setState({
labels: {
inputTagValue: '',
tags: [...this.state.labels.tags, ...newTag],
tagError: '',
}
});
}

render() {
let errorObject = [this.rules.appName(this.state.form.appName), this.rules.team(this.state.form.projectId)];
let errorObject = [this.rules.appName(this.state.form.appName), this.rules.team(this.state.form.projectId)]
let showError = this.state.showErrors;
let provider = this.state.projects.find(project => this.state.form.projectId === project.id);
let clone = this.state.apps.find(app => this.state.form.cloneId === app.id);
Expand Down Expand Up @@ -192,14 +279,26 @@ export class AddNewApp extends Component<AddNewAppProps, AddNewAppState> {
})}
</Select>
</div>
{this.state.form.cloneId > 0 && <div className="info__container info__container--create-app">
<TagLabelSelect
validateTags={validateTags}
labelTags={this.state.labels}
onInputChange={this.handleInputChange}
onTagsChange={this.handleTagsChange}
onKeyDown={(event)=>handleKeyDown(this.state.labels,this.setAppTagLabel, event)}
onCreatableBlur={this.handleCreatableBlur}
/>

<div className="cr-5 fs-11">{this.state.labels.tagError}</div>
{this.state.form.cloneId > 0 && <div className="mt-20 info__container info__container--create-app">
<Info />
<div className="flex column left">
<div className="info__title">Important</div>
<div className="info__subtitle">Do not forget to modify git repositories, corresponding branches and docker repositories to be used for each CI Pipeline if required.</div>
</div>
</div>}
<DialogFormSubmit tabIndex={3}>{this.state.form.cloneId > 0 ? 'Duplicate App' : 'Create App'}</DialogFormSubmit>
<div className=" mt-40">
<DialogFormSubmit tabIndex={3}>{this.state.form.cloneId > 0 ? 'Duplicate App' : 'Create App'}</DialogFormSubmit>
</div>
</DialogForm >
}
}
Expand Down
1 change: 0 additions & 1 deletion src/components/app/create/validationRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ export class ValidationRules {
if (found) return { isValid: true, message: '' };
else return { isValid: false, message: 'This is a required field' };
}

}
64 changes: 64 additions & 0 deletions src/components/app/details/AboutAppInfoModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react'
import { ReactComponent as Close } from '../../../assets/icons/ic-close.svg';
import moment from 'moment'
import { Moment12HourFormat } from '../../../config';
import { Progressing } from '../../common';
import TagLabelSelect from './TagLabelSelect';
import { ReactComponent as Error } from '../../../assets/icons/ic-warning.svg';
import { validateTags } from '../appLabelCommon'

export default function AboutAppInfoModal({ onClose, appMetaResult, isLoading, labelTags, handleInputChange, handleTagsChange, handleKeyDown, handleCreatableBlur, handleSubmit, submitting }) {

const renderAboutModalInfoHeader = () => {
return <div className="modal__header">
<div className="fs-20 cn-9 fw-6">About</div>
<button className="transparent" onClick={() => onClose(false)}>
<Close className="icon-dim-24 cursor" />
</button>
</div>
}

const renderValidationMessaging = () => {
if (labelTags.tagError !== "") {
return <div className="cr-5 fs-11">
<Error className="form__icon form__icon--error" />{labelTags.tagError}
</div>
}
}

const renderAboutModalInfo = () => {
return <div>
<div className="pt-12">
<div className="cn-6 fs-12 mb-2">App name</div>
<div className="cn-9 fs-14 mb-16">{appMetaResult?.appName}</div>
</div>
<div>
<div className="cn-6 fs-12 mb-2">Created on</div>
<div className="cn-9 fs-14 mb-16">{moment(appMetaResult?.createdOn).format(Moment12HourFormat)}</div>
</div>
<div>
<div className="cn-6 fs-12 mb-2">Created by</div>
<div className="cn-9 fs-14 mb-16">{appMetaResult?.createdBy}</div>
</div>
<div>
<div className="cn-6 fs-12 mb-2">Project</div>
<div className="cn-9 fs-14 mb-16">{appMetaResult?.projectName}</div>
</div>
<TagLabelSelect validateTags={validateTags} labelTags={labelTags} onInputChange={handleInputChange} onTagsChange={handleTagsChange} onKeyDown={handleKeyDown} onCreatableBlur={handleCreatableBlur} />
{renderValidationMessaging()}
<div className='form__buttons mt-40'>
<button className=' cta' type="submit" disabled={submitting} onClick={(e) => { e.preventDefault(); handleSubmit(e) }} tabIndex={5} >
{submitting ? <Progressing /> : ' Save'}
</button>
</div>
</div>
}

return (<div>
{renderAboutModalInfoHeader()}
{isLoading ? <div className="flex" style={{ minHeight: "400px" }}>
<Progressing pageLoader />
</div> : renderAboutModalInfo()}
</div>
)
}
63 changes: 63 additions & 0 deletions src/components/app/details/TagLabelSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React,{useMemo} from 'react';
import Creatable from 'react-select/creatable';
import { ClearIndicator, MultiValueRemove, MultiValueChipContainer } from '../../common';

export default function TagLabelSelect({ validateTags, labelTags, onInputChange, onKeyDown, onTagsChange, onCreatableBlur }) {
const creatableOptions = useMemo(() => ([]), [])
const CreatableChipStyle = {
multiValue: (base, state) => {
return ({
...base,
border: validateTags(state.data.value) ? `1px solid var(--N200)` : `1px solid var(--R500)`,
borderRadius: `4px`,
background: validateTags(state.data.value) ? 'white' : 'var(--R100)',
height: '28px',
margin: '8px 8px 4px 0px',
paddingLeft: '4px',
fontSize: '12px',
})
},
control: (base, state) => ({
...base,
border: state.isFocused ? '1px solid #06c' : '1px solid #d0d4d9', // default border color
boxShadow: 'none', // no box-shadow
minHeight: '72px',
alignItems: "end",
}),
indicatorsContainer: () => ({
height: '28px'
})
}

return (
<div>
<span className="form__label cn-6"> Tags (only key:value allowed)</span>
<Creatable
className={"create-app_tags"}
options={creatableOptions}
components={{
DropdownIndicator: () => null,
ClearIndicator,
MultiValueRemove,
MultiValueContainer: ({ ...props }) => <MultiValueChipContainer {...props} validator={validateTags} />,
IndicatorSeparator: () => null,
Menu: () => null,
}}
styles={CreatableChipStyle}
autoFocus
isMulti
isClearable
inputValue={labelTags.inputTagValue}
placeholder="Add a tag..."
isValidNewOption={() => false}
backspaceRemovesValue
value={labelTags.tags}
onBlur={onCreatableBlur}
onInputChange={onInputChange}
onKeyDown={onKeyDown}
onChange={onTagsChange}
/>
</div>
)
}

9 changes: 8 additions & 1 deletion src/components/app/details/app.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
.tab-list__tab-link.active .tab-list__icon path {
fill: var(--B500);
}
}

.tab-list__info-icon + .tab-list__info{
min-width: 400px;
position: absolute;
top: 40px;
margin-left: 140px;
}
Loading

0 comments on commit e0f152c

Please sign in to comment.